chati-dev 4.5.6 → 4.5.8

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 (45) hide show
  1. package/framework/agents/build/dev.md +1 -1
  2. package/framework/agents/deploy/devops.md +1 -1
  3. package/framework/agents/discover/brief.md +1 -1
  4. package/framework/agents/discover/brownfield-wu.md +1 -1
  5. package/framework/agents/discover/greenfield-wu.md +1 -1
  6. package/framework/agents/plan/architect-data-engineer.md +1 -1
  7. package/framework/agents/plan/architect-system.md +1 -1
  8. package/framework/agents/plan/architect.md +1 -1
  9. package/framework/agents/plan/detail.md +1 -1
  10. package/framework/agents/plan/phases.md +1 -1
  11. package/framework/agents/plan/tasks.md +1 -1
  12. package/framework/agents/plan/ux-brand-architect.md +1 -1
  13. package/framework/agents/plan/ux-component-engineer.md +1 -1
  14. package/framework/agents/plan/ux-researcher.md +1 -1
  15. package/framework/agents/plan/ux.md +1 -1
  16. package/framework/agents/quality/qa-implementation.md +1 -1
  17. package/framework/agents/quality/qa-planning.md +1 -1
  18. package/framework/agents/quality/qa-visual.md +1 -1
  19. package/framework/agents/shared/visualizer.md +1 -1
  20. package/framework/config.yaml +2 -2
  21. package/framework/context/root.md +1 -1
  22. package/framework/data/entity-registry.yaml +1 -1
  23. package/framework/hooks/license-guard.js +19 -0
  24. package/framework/manifest.json +55 -50
  25. package/framework/manifest.sig +1 -1
  26. package/framework/orchestrator/chati-router.js +177 -71
  27. package/framework/orchestrator/chati.md +1 -1
  28. package/framework/package.json +3 -0
  29. package/framework/schemas/session.schema.json +26 -9
  30. package/package.json +2 -1
  31. package/src/config/claude-settings-generator.js +6 -6
  32. package/src/installer/core.js +55 -25
  33. package/src/installer/path-replacement.js +13 -0
  34. package/src/installer/provider-overlay.js +3 -1
  35. package/src/installer-v2/index.js +55 -0
  36. package/src/orchestrator/cli.js +9 -6
  37. package/src/orchestrator/session-manager.js +134 -96
  38. package/src/terminal/adapters/claude-adapter.js +9 -2
  39. package/src/terminal/adapters/codex-adapter.js +9 -2
  40. package/src/terminal/adapters/gemini-adapter.js +5 -2
  41. package/src/terminal/adapters/grok-adapter.js +13 -3
  42. package/src/terminal/cli-registry.js +5 -0
  43. package/src/terminal/run-agent.js +5 -0
  44. package/src/terminal/run-parallel.js +7 -0
  45. package/src/terminal/spawner.js +49 -10
@@ -21,6 +21,125 @@ import { join } from 'path';
21
21
  const PROJECT_DIR = process.cwd();
22
22
  const ROUTER_ARGS = process.argv.slice(2);
23
23
 
24
+ async function readEnabledHarnesses(projectDir) {
25
+ const installationPath = join(projectDir, '.chati', 'v2', 'installation.json');
26
+ if (existsSync(installationPath)) {
27
+ try {
28
+ const artifact = JSON.parse(readFileSync(installationPath, 'utf-8'));
29
+ const fwDir = existsSync(join(projectDir, '.chati.dev')) ? '.chati.dev' : 'chati.dev';
30
+ const doctorModulePath = join(projectDir, fwDir, '_cli', 'installer-v2', 'index.js');
31
+ if (!existsSync(doctorModulePath)) return { harnesses: [], error: 'v2 doctor missing' };
32
+ const { doctorV2 } = await import(doctorModulePath);
33
+ if (!doctorV2(artifact).passed) return { harnesses: [], error: 'v2 artifact invalid' };
34
+ const harnesses = artifact.installation?.enabled_providers
35
+ ?.map((binding) => binding.harness_id)
36
+ .filter(Boolean);
37
+ if (harnesses?.length) return { harnesses: [...new Set(harnesses)], error: null };
38
+ } catch { return { harnesses: [], error: 'v2 artifact invalid' }; }
39
+ }
40
+
41
+ const detected = [];
42
+ if (existsSync(join(projectDir, '.claude', 'commands', 'chati.md'))) detected.push('claude');
43
+ if (existsSync(join(projectDir, '.agents', 'skills', 'chati', 'SKILL.md'))) detected.push('codex');
44
+ if (existsSync(join(projectDir, '.grok', 'commands', 'chati.md'))) detected.push('grok');
45
+ return { harnesses: detected, error: null };
46
+ }
47
+
48
+ function hasContent(path) {
49
+ return existsSync(path) && readFileSync(path, 'utf-8').trim().length > 0;
50
+ }
51
+
52
+ function codexHooksEnabled(config) {
53
+ if (/^\s*features\.hooks\s*=\s*true\s*(?:#.*)?$/m.test(config)) return true;
54
+ const lines = config.split('\n');
55
+ const sectionStart = lines.findIndex((line) => /^\s*\[features\]\s*(?:#.*)?$/.test(line));
56
+ if (sectionStart === -1) return false;
57
+ const sectionEnd = lines.findIndex((line, index) => index > sectionStart && /^\s*\[.*\]\s*(?:#.*)?$/.test(line));
58
+ const end = sectionEnd === -1 ? lines.length : sectionEnd;
59
+ return lines.slice(sectionStart + 1, end).some((line) => /^\s*hooks\s*=\s*true\s*(?:#.*)?$/.test(line));
60
+ }
61
+
62
+ export async function validateProviderIntegrity(projectDir) {
63
+ const missing = [];
64
+ const { harnesses, error } = await readEnabledHarnesses(projectDir);
65
+
66
+ if (error) missing.push(error);
67
+ const supportedHarnesses = new Set(['claude', 'codex', 'grok']);
68
+ for (const harness of harnesses) {
69
+ if (!supportedHarnesses.has(harness)) missing.push(`unsupported harness ${harness}`);
70
+ }
71
+
72
+ if (harnesses.length === 0) missing.push('provider entry point');
73
+
74
+ if (harnesses.includes('claude')) {
75
+ const commandPath = join(projectDir, '.claude', 'commands', 'chati.md');
76
+ const settingsPath = join(projectDir, '.claude', 'settings.json');
77
+ if (!hasContent(commandPath)) missing.push('claude command');
78
+ if (!existsSync(settingsPath)) {
79
+ missing.push('claude settings');
80
+ } else {
81
+ try {
82
+ const settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
83
+ const ptu = settings.hooks?.PreToolUse || [];
84
+ const ups = settings.hooks?.UserPromptSubmit || [];
85
+ const ptuCmds = ptu.flatMap((group) => (group.hooks || []).map((hook) => hook.command || ''));
86
+ const upsCmds = ups.flatMap((group) => (group.hooks || []).map((hook) => hook.command || ''));
87
+ missing.push(...['mode-governance', 'constitution-guard', 'read-protection']
88
+ .filter((hook) => !ptuCmds.some((command) => command.includes(hook)))
89
+ .map((hook) => `claude hook ${hook}`));
90
+ if (![...upsCmds, ...ptuCmds].some((command) => command.includes('license-guard'))) {
91
+ missing.push('claude hook license-guard');
92
+ }
93
+ } catch {
94
+ missing.push('claude settings invalid');
95
+ }
96
+ }
97
+ }
98
+
99
+ if (harnesses.includes('codex')) {
100
+ const required = [
101
+ ['codex skill', '.agents/skills/chati/SKILL.md'],
102
+ ['codex constitution guard', '.codex/rules/constitution-guard.rules'],
103
+ ['codex read protection', '.codex/rules/read-protection.rules'],
104
+ ];
105
+ for (const [label, relativePath] of required) {
106
+ if (!hasContent(join(projectDir, relativePath))) missing.push(label);
107
+ }
108
+ const configPath = join(projectDir, '.codex', 'config.toml');
109
+ if (!hasContent(configPath) || !codexHooksEnabled(readFileSync(configPath, 'utf-8'))) {
110
+ missing.push('codex hooks feature');
111
+ }
112
+ const hooksPath = join(projectDir, '.codex', 'hooks.json');
113
+ if (!hasContent(hooksPath)) {
114
+ missing.push('codex license hook');
115
+ } else {
116
+ try {
117
+ const hooks = JSON.parse(readFileSync(hooksPath, 'utf-8'));
118
+ const validHook = (hooks.hooks?.UserPromptSubmit || []).some((group) =>
119
+ group.matcher === '.*'
120
+ && (group.hooks || []).some((hook) =>
121
+ hook.type === 'command'
122
+ && hook.command === 'node .chati.dev/hooks/license-guard.js'
123
+ && hook.async !== true
124
+ )
125
+ );
126
+ if (!validHook) {
127
+ missing.push('codex license hook');
128
+ }
129
+ } catch {
130
+ missing.push('codex hooks invalid');
131
+ }
132
+ }
133
+ }
134
+
135
+ if (harnesses.includes('grok')) {
136
+ if (!hasContent(join(projectDir, '.grok', 'commands', 'chati.md'))) missing.push('grok command');
137
+ if (!hasContent(join(projectDir, '.grok', 'session-lock.md'))) missing.push('grok session lock');
138
+ }
139
+
140
+ return { valid: missing.length === 0, missing };
141
+ }
142
+
24
143
  /**
25
144
  * Find the chati-dev CLI module. Tries local node_modules first (fast),
26
145
  * then monorepo dev layout. Returns the module or null.
@@ -245,9 +364,64 @@ async function dispatchOrchestrate(subCommand, argv) {
245
364
  }
246
365
  }
247
366
 
367
+ async function runSecurityPreflight(projectDir) {
368
+ const result = { ok: true, license: null, integrity: true, error: null };
369
+ try {
370
+ const integrity = await validateProviderIntegrity(projectDir);
371
+ if (!integrity.valid) {
372
+ return {
373
+ ...result,
374
+ ok: false,
375
+ integrity: false,
376
+ error: `integrity_failed: missing ${integrity.missing.join(', ')}`,
377
+ };
378
+ }
379
+ } catch {
380
+ return { ...result, ok: false, integrity: false, error: 'integrity_failed' };
381
+ }
382
+
383
+ try {
384
+ const fwDir = existsSync(join(projectDir, '.chati.dev')) ? '.chati.dev' : 'chati.dev';
385
+ const hookPath = join(projectDir, fwDir, 'hooks', 'license-guard.js');
386
+ if (!existsSync(hookPath)) {
387
+ return {
388
+ ...result,
389
+ ok: false,
390
+ error: 'license_guard_missing',
391
+ license: { valid: false, reason: 'License enforcement file missing. Reinstall with: npx chati-dev init' },
392
+ };
393
+ }
394
+ const mod = await import(hookPath);
395
+ result.license = await mod.checkLicense();
396
+ if (!result.license.valid) return { ...result, ok: false, error: 'license_invalid' };
397
+ } catch {
398
+ return {
399
+ ...result,
400
+ ok: false,
401
+ error: 'license_check_error',
402
+ license: { valid: false, error: 'license_check_error' },
403
+ };
404
+ }
405
+
406
+ return result;
407
+ }
408
+
409
+ // Only read-only diagnosis and license recovery may run without a valid
410
+ // installation preflight. `init` mutates session state and must never bypass
411
+ // license or provider-integrity enforcement.
412
+ const PREFLIGHT_EXEMPT_SUBS = new Set(['doctor', 'wait-for-license']);
413
+
248
414
  async function main() {
249
415
  const subCommand = ROUTER_ARGS[0];
250
416
 
417
+ const preflight = subCommand && PREFLIGHT_EXEMPT_SUBS.has(subCommand)
418
+ ? { ok: true, license: null, integrity: true, error: null }
419
+ : await runSecurityPreflight(PROJECT_DIR);
420
+ if (!preflight.ok) {
421
+ console.log(JSON.stringify(preflight));
422
+ return;
423
+ }
424
+
251
425
  // Sub-command mode: `node chati-router.js <subcommand> [--flags...]`
252
426
  // Routes to the local CLI for ANY orchestrate subcommand. No npx, no network.
253
427
  if (subCommand && VALID_ORCHESTRATE_SUBS.has(subCommand)) {
@@ -271,8 +445,8 @@ async function main() {
271
445
  // No-subcommand mode (default): full router pipeline (license + integrity + session + next)
272
446
  const result = {
273
447
  ok: true,
274
- license: null,
275
- integrity: true,
448
+ license: preflight.license,
449
+ integrity: preflight.integrity,
276
450
  session: null,
277
451
  action: null,
278
452
  pipeline: null,
@@ -281,75 +455,7 @@ async function main() {
281
455
  };
282
456
 
283
457
  // -----------------------------------------------------------------------
284
- // 1. License validation
285
- // -----------------------------------------------------------------------
286
- try {
287
- const fwDir = existsSync(join(PROJECT_DIR, '.chati.dev')) ? '.chati.dev' : 'chati.dev';
288
- const hookPath = join(PROJECT_DIR, fwDir, 'hooks', 'license-guard.js');
289
- if (!existsSync(hookPath)) {
290
- result.ok = false;
291
- result.error = 'license_guard_missing';
292
- result.license = { valid: false, reason: 'License enforcement file missing. Reinstall with: npx chati-dev init' };
293
- console.log(JSON.stringify(result));
294
- return;
295
- }
296
-
297
- const mod = await import(hookPath);
298
- const license = await mod.checkLicense();
299
- result.license = license;
300
-
301
- if (!license.valid) {
302
- result.ok = false;
303
- result.error = 'license_invalid';
304
- console.log(JSON.stringify(result));
305
- return;
306
- }
307
- } catch (err) {
308
- // Fail-closed on license check errors
309
- result.license = { valid: false, error: 'license_check_error' };
310
- result.ok = false;
311
- }
312
-
313
- // -----------------------------------------------------------------------
314
- // 2. Integrity check — are all required hooks registered in settings.json?
315
- // Note: license-guard is registered under UserPromptSubmit (not PreToolUse).
316
- // mode-governance, constitution-guard, and read-protection are in PreToolUse.
317
- // -----------------------------------------------------------------------
318
- try {
319
- const settingsPath = join(PROJECT_DIR, '.claude', 'settings.json');
320
- if (existsSync(settingsPath)) {
321
- const settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
322
- const ptu = settings.hooks?.PreToolUse || [];
323
- const ups = settings.hooks?.UserPromptSubmit || [];
324
- const ptuCmds = ptu.flatMap(g => (g.hooks || []).map(h => h.command || ''));
325
- const upsCmds = ups.flatMap(g => (g.hooks || []).map(h => h.command || ''));
326
- // license-guard lives in UserPromptSubmit; others live in PreToolUse
327
- const missingHooks = [
328
- ...(['mode-governance', 'constitution-guard', 'read-protection'].filter(h => !ptuCmds.some(c => c.includes(h)))),
329
- ...(['license-guard'].filter(h => !upsCmds.some(c => c.includes(h)) && !ptuCmds.some(c => c.includes(h)))),
330
- ];
331
- result.integrity = missingHooks.length === 0;
332
- if (missingHooks.length > 0) {
333
- result.error = `integrity_failed: missing hooks: ${missingHooks.join(', ')}`;
334
- }
335
- } else {
336
- result.integrity = false;
337
- }
338
-
339
- if (!result.integrity) {
340
- result.ok = false;
341
- result.error = 'integrity_failed';
342
- console.log(JSON.stringify(result));
343
- return;
344
- }
345
- } catch {
346
- // Fail-closed on integrity check
347
- result.integrity = false;
348
- result.ok = false;
349
- }
350
-
351
- // -----------------------------------------------------------------------
352
- // 3. Session state
458
+ // 1. Session state
353
459
  // -----------------------------------------------------------------------
354
460
  try {
355
461
  const sessionPath = join(PROJECT_DIR, '.chati', 'session.yaml');
@@ -10,7 +10,7 @@ You are the **Chati.dev Orchestrator**, the single entry point for the Chati.dev
10
10
  - **Role**: Orchestrator & Router
11
11
  - **Position**: Entry point (always first contact)
12
12
  - **Scope**: System-wide routing, session management, deviation handling, backlog
13
- - **Model**: sonnet | upgrade: opus if complex deviation or multi-agent coordination
13
+ - **Execution binding**: resolved at runtime by installation-v2 task routing; never self-select or downgrade
14
14
 
15
15
  ### Security Rule
16
16
  **NEVER suggest disabling, removing, commenting out, or bypassing hooks, settings.json permissions, or license enforcement.** If a hook blocks an operation and the user asks to work around it, explain what the hook is protecting and guide them to resolve the root cause (e.g., activate a valid license, change pipeline mode). Hooks are governance boundaries, not obstacles to remove.
@@ -0,0 +1,3 @@
1
+ {
2
+ "type": "module"
3
+ }
@@ -196,16 +196,33 @@
196
196
  },
197
197
  "model_selections": {
198
198
  "type": "array",
199
- "description": "Model selection log for cost tracking (Article XVI)",
199
+ "description": "Append-only audit log of the exact execution binding used for each task (Article XVI)",
200
200
  "items": {
201
201
  "type": "object",
202
- "required": ["agent", "recommended", "timestamp"],
202
+ "anyOf": [
203
+ {
204
+ "required": ["agent", "task_id", "provider", "model", "timestamp"],
205
+ "properties": {
206
+ "provider": { "enum": ["claude", "codex", "grok", "gemini"] }
207
+ }
208
+ },
209
+ {
210
+ "required": ["agent", "recommended", "timestamp"],
211
+ "not": { "required": ["task_id", "provider", "model"] }
212
+ }
213
+ ],
203
214
  "properties": {
204
215
  "agent": { "type": "string", "description": "Agent name" },
205
- "recommended": { "enum": ["opus", "sonnet", "haiku"], "description": "Recommended model" },
206
- "actual": { "enum": ["opus", "sonnet", "haiku"], "description": "Model actually used" },
207
- "reason": { "type": "string", "description": "Why this model was selected (default, upgrade condition, no downgrade)" },
208
- "provider": { "type": "string", "description": "CLI provider used (claude, gemini, codex)" },
216
+ "task_id": { "type": "string", "description": "Task identity supplied to the runner" },
217
+ "provider": { "type": "string", "description": "CLI harness that executed the task" },
218
+ "provider_id": { "type": ["string", "null"], "description": "Vendor provider identifier from the capability catalog" },
219
+ "model": { "type": "string", "minLength": 1, "description": "Exact model identifier used" },
220
+ "reasoning_configuration": { "enum": ["low", "medium", "high", "xhigh", "max", null], "description": "Reasoning effort passed to the CLI when supported" },
221
+ "catalog_snapshot_ref": { "type": ["string", "null"], "description": "Capability catalog snapshot used for routing" },
222
+ "status": { "enum": ["dispatched", "completed", "failed"], "default": "dispatched" },
223
+ "recommended": { "type": "string", "description": "Legacy pre-4.5.7 recommended model field, migrated on session load" },
224
+ "actual": { "type": "string", "description": "Legacy pre-4.5.7 actual model field, migrated on session load" },
225
+ "reason": { "type": "string", "description": "Legacy pre-4.5.7 selection reason" },
209
226
  "timestamp": { "type": "string", "format": "date-time" }
210
227
  }
211
228
  }
@@ -367,13 +384,13 @@
367
384
  "active_model": {
368
385
  "type": ["string", "null"],
369
386
  "default": null,
370
- "description": "Normalized Claude model key (Article XII v2 / context-window-detection v1). Output of parseClaudeModelId — e.g. 'opus', 'opus[1m]', 'sonnet', 'haiku'. Written at /chati boot; drives context-limit resolution in hooks. Null = unknown (detection chain falls through to provider default)."
387
+ "description": "Model identifier detected for the current interactive harness. Written at /chati boot and used for context-window resolution. Per-task routed models are recorded separately in model_selections[]."
371
388
  },
372
389
  "active_provider": {
373
390
  "type": ["string", "null"],
374
- "enum": ["claude", "gemini", "codex", null],
391
+ "enum": ["claude", "gemini", "codex", "grok", null],
375
392
  "default": null,
376
- "description": "Active CLI provider. Redundant with providers_enabled[0] but flat for hook convenience."
393
+ "description": "Current interactive CLI harness. This is independent from the per-task provider recorded in model_selections[]."
377
394
  },
378
395
  "context_tokens_used": {
379
396
  "type": "integer",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chati-dev",
3
- "version": "4.5.6",
3
+ "version": "4.5.8",
4
4
  "description": "AI-Powered Multi-Agent Orchestration System - Structured vibe coding for Full Stack Development",
5
5
  "type": "module",
6
6
  "bin": {
@@ -77,6 +77,7 @@
77
77
  "chalk": "^5.3.0",
78
78
  "js-yaml": "^5.4.0",
79
79
  "ora": "^8.0.1",
80
+ "proper-lockfile": "4.1.2",
80
81
  "semver": "^7.6.0"
81
82
  },
82
83
  "bundleDependencies": [
@@ -139,12 +139,12 @@ const PERMISSIONS_DENY = [
139
139
  'Bash(git rebase --no-verify:*)',
140
140
 
141
141
  // System path writes — `//` prefix = absolute filesystem
142
- 'Write(//etc/**)',
143
- 'Write(//usr/**)',
144
- 'Write(//System/**)',
145
- 'Write(//bin/**)',
146
- 'Write(//sbin/**)',
147
- 'Write(//Library/**)',
142
+ 'Edit(//etc/**)',
143
+ 'Edit(//usr/**)',
144
+ 'Edit(//System/**)',
145
+ 'Edit(//bin/**)',
146
+ 'Edit(//sbin/**)',
147
+ 'Edit(//Library/**)',
148
148
  'Edit(//etc/**)',
149
149
  'Edit(//usr/**)',
150
150
  'Edit(//System/**)',
@@ -1,6 +1,6 @@
1
1
  import { mkdirSync, writeFileSync, copyFileSync, existsSync, readFileSync, readdirSync, statSync } from 'fs';
2
2
  import { execFileSync } from 'child_process';
3
- import { join, dirname, basename } from 'path';
3
+ import { join, dirname } from 'path';
4
4
  import { fileURLToPath } from 'url';
5
5
  import { createRequire } from 'module';
6
6
  import { IDE_CONFIGS, IDE_TO_PROVIDER } from '../config/ide-configs.js';
@@ -10,6 +10,7 @@ import { generateContextFiles } from '../config/context-file-generator.js';
10
10
  import { adaptFrameworkFile, ADAPTABLE_FILES } from '../config/framework-adapter.js';
11
11
  import { generateClaudeSettings } from '../config/claude-settings-generator.js';
12
12
  import { generateProviderOverlays } from './provider-overlay.js';
13
+ import { applyInstalledPathReplacement } from './path-replacement.js';
13
14
  import { verifyManifest } from './manifest.js';
14
15
 
15
16
  const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -192,7 +193,7 @@ export async function installFramework(config) {
192
193
  if (existsSync(src)) {
193
194
  // Apply path replacement so rules reference .chati.dev/ not chati.dev/
194
195
  let content = readFileSync(src, 'utf-8');
195
- content = applyPathReplacement(content);
196
+ content = applyInstalledPathReplacement(content);
196
197
  writeFileSync(join(claudeRulesDir, file), content, 'utf-8');
197
198
  }
198
199
  }
@@ -269,6 +270,7 @@ export const FRAMEWORK_DIRS_TO_COPY = [
269
270
  */
270
271
  const FRAMEWORK_ROOT_FILES = [
271
272
  'constitution.md',
273
+ 'package.json',
272
274
  ];
273
275
 
274
276
  /**
@@ -287,20 +289,6 @@ const TEXT_EXTENSIONS = new Set(['.md', '.js', '.yaml', '.yml', '.json', '.html'
287
289
  * Order matters: replace `chati.dev/artifacts/` FIRST (→ `artifacts/`),
288
290
  * then `chati.dev/` (→ `.chati.dev/`). Reversing would produce `.chati.dev/artifacts/`.
289
291
  */
290
- function applyPathReplacement(content) {
291
- return content
292
- // Step 1: artifacts at root (must be before generic replace)
293
- .replace(/chati\.dev\/artifacts\//g, 'artifacts/')
294
- // Step 2: path strings with slash — but NOT if already prefixed with dot
295
- // (negative lookbehind for any char that would create `.chati.dev` already)
296
- .replace(/(?<!\.)chati\.dev\//g, '.chati.dev/')
297
- // Step 3: quoted dir name in JS join() calls (WITHOUT slash)
298
- // Only match 'chati.dev' (the DIRECTORY, with dot) not 'chati-dev' (the NPM PACKAGE, with dash)
299
- // Also: avoid double-prefixing if already '.chati.dev' in source
300
- .replace(/(?<!\.)'chati\.dev'/g, "'.chati.dev'")
301
- .replace(/(?<!\.)"chati\.dev"/g, '".chati.dev"');
302
- }
303
-
304
292
  /**
305
293
  * Recursively copy a directory from source to destination, applying:
306
294
  * 1. Path replacement (chati.dev/ → .chati.dev/, artifacts/ at root)
@@ -327,7 +315,7 @@ function copyDirRecursive(srcDir, destDir, provider, relBase = '') {
327
315
  if (isText) {
328
316
  let content = readFileSync(srcPath, 'utf-8');
329
317
  // Always apply path replacement for installed location
330
- content = applyPathReplacement(content);
318
+ content = applyInstalledPathReplacement(content);
331
319
  // Provider adaptation on top (for non-Claude)
332
320
  if (provider !== 'claude' && ADAPTABLE_FILES.has(relPath)) {
333
321
  content = adaptFrameworkFile(content, relPath, provider);
@@ -360,7 +348,7 @@ export function copyFrameworkFiles(destDir, provider = 'claude') {
360
348
  const dest = join(destDir, file);
361
349
  createDir(dirname(dest));
362
350
  let content = readFileSync(src, 'utf-8');
363
- content = applyPathReplacement(content);
351
+ content = applyInstalledPathReplacement(content);
364
352
  if (provider !== 'claude' && ADAPTABLE_FILES.has(file)) {
365
353
  content = adaptFrameworkFile(content, file, provider);
366
354
  }
@@ -448,17 +436,18 @@ Parse the JSON output. This single command already handled: license validation,
448
436
 
449
437
  // Starlark execution policies (Codex sandboxed rule engine)
450
438
  createDir(join(targetDir, '.codex', 'rules'));
439
+ writeCodexConfigWithHooks(join(targetDir, '.codex', 'config.toml'));
451
440
  writeFileSync(join(targetDir, '.codex', 'rules', 'constitution-guard.rules'), generateCodexConstitutionGuardRules(), 'utf-8');
452
441
  writeFileSync(join(targetDir, '.codex', 'rules', 'read-protection.rules'), generateCodexReadProtectionRules(), 'utf-8');
453
442
 
454
443
  // .codex/hooks.json — license-guard wired to UserPromptSubmit (per-turn enforcement).
455
- // Codex hooks.json is experimental (requires [features] codex_hooks = true in
456
- // codex config). Without it, license enforcement only happens at slash command
444
+ // Codex hooks require [features] hooks = true in the active project config.
445
+ // Without it, license enforcement only happens at skill entry and the router,
457
446
  // entry — a long-running terminal session would not be re-validated.
458
447
  writeFileSync(
459
448
  join(targetDir, '.codex', 'hooks.json'),
460
449
  JSON.stringify({
461
- $comment: 'chati.dev v4.2.2 experimental Codex hooks. Requires [features] codex_hooks = true.',
450
+ $comment: 'chati.dev Codex lifecycle hooks. Requires [features] hooks = true.',
462
451
  hooks: {
463
452
  UserPromptSubmit: [
464
453
  {
@@ -486,7 +475,7 @@ Parse the JSON output. This single command already handled: license validation,
486
475
  for (const file of contextFileNames) {
487
476
  const src = join(FRAMEWORK_SOURCE, 'context', file);
488
477
  if (existsSync(src)) {
489
- const content = readFileSync(src, 'utf-8');
478
+ const content = applyInstalledPathReplacement(readFileSync(src, 'utf-8'));
490
479
  writeFileSync(join(geminiContextDir, file), adaptFrameworkFile(content, `context/${file}`, 'gemini'), 'utf-8');
491
480
  }
492
481
  }
@@ -515,6 +504,38 @@ Parse the JSON output. This single command already handled: license validation,
515
504
  }
516
505
  }
517
506
 
507
+ export function writeCodexConfigWithHooks(configPath) {
508
+ createDir(dirname(configPath));
509
+ if (!existsSync(configPath)) {
510
+ writeFileSync(configPath, '[features]\nhooks = true\n', 'utf-8');
511
+ return;
512
+ }
513
+
514
+ const original = readFileSync(configPath, 'utf-8');
515
+ if (/^\s*features\.hooks\s*=\s*true\s*(?:#.*)?$/m.test(original)) return;
516
+ if (/^\s*features\.hooks\s*=/m.test(original)) {
517
+ writeFileSync(configPath, original.replace(/^\s*features\.hooks\s*=.*$/m, 'features.hooks = true'), 'utf-8');
518
+ return;
519
+ }
520
+
521
+ const lines = original.split('\n');
522
+ const sectionStart = lines.findIndex((line) => /^\s*\[features\]\s*(?:#.*)?$/.test(line));
523
+ if (sectionStart === -1) {
524
+ const prefix = original.endsWith('\n') ? original : `${original}\n`;
525
+ writeFileSync(configPath, `${prefix}\n[features]\nhooks = true\n`, 'utf-8');
526
+ return;
527
+ }
528
+
529
+ let sectionEnd = lines.findIndex((line, index) => index > sectionStart && /^\s*\[.*\]\s*(?:#.*)?$/.test(line));
530
+ if (sectionEnd === -1) sectionEnd = lines.length;
531
+ const hookIndex = lines.findIndex((line, index) =>
532
+ index > sectionStart && index < sectionEnd && /^\s*hooks\s*=/.test(line)
533
+ );
534
+ if (hookIndex !== -1) lines[hookIndex] = 'hooks = true';
535
+ else lines.splice(sectionEnd, 0, 'hooks = true');
536
+ writeFileSync(configPath, lines.join('\n'), 'utf-8');
537
+ }
538
+
518
539
  /**
519
540
  * Generate provider-agnostic instructions file content.
520
541
  * Used for non-Claude IDEs (.vscode/chati/rules.md, .cursorrules, etc.)
@@ -578,6 +599,10 @@ export function updateGitignore(targetDir, selectedIDEs) {
578
599
  '.chati/memories/*/session/',
579
600
  '# NOTE: durable/ and daily/ memories ARE committed — they contain project knowledge',
580
601
  '',
602
+ '# Operating system metadata',
603
+ '.DS_Store',
604
+ '._*',
605
+ '',
581
606
  '# Claude Code integration (framework portions only)',
582
607
  '.claude/commands/chati.md',
583
608
  '.claude/rules/chati/',
@@ -822,9 +847,14 @@ export function copyCliDependencies(pkgDir, destNodeModules) {
822
847
  let p = require.resolve(depName);
823
848
  while (p && p !== dirname(p)) {
824
849
  p = dirname(p);
825
- if (existsSync(join(p, 'package.json')) && basename(p) === depName.split('/').pop()) {
826
- depDir = p;
827
- break;
850
+ const candidatePackage = join(p, 'package.json');
851
+ if (existsSync(candidatePackage)) {
852
+ try {
853
+ if (JSON.parse(readFileSync(candidatePackage, 'utf-8')).name === depName) {
854
+ depDir = p;
855
+ break;
856
+ }
857
+ } catch { /* keep walking */ }
828
858
  }
829
859
  }
830
860
  } catch { continue; }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Translate repository paths into their installed-project locations.
3
+ *
4
+ * Source files live under `chati.dev/`. Installed framework files live under
5
+ * `.chati.dev/`, while user-facing artifacts remain at project root.
6
+ */
7
+ export function applyInstalledPathReplacement(content) {
8
+ return content
9
+ .replace(/chati\.dev\/artifacts\//g, 'artifacts/')
10
+ .replace(/(?<!\.)chati\.dev\//g, '.chati.dev/')
11
+ .replace(/(?<!\.)'chati\.dev'/g, "'.chati.dev'")
12
+ .replace(/(?<!\.)"chati\.dev"/g, '".chati.dev"');
13
+ }
@@ -13,6 +13,7 @@
13
13
  import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
14
14
  import { join, dirname } from 'path';
15
15
  import { adaptFrameworkFile, ADAPTABLE_FILES } from '../config/framework-adapter.js';
16
+ import { applyInstalledPathReplacement } from './path-replacement.js';
16
17
 
17
18
  /**
18
19
  * Generate adapted framework overlays for all enabled providers.
@@ -41,7 +42,8 @@ export function generateProviderOverlays(targetDir, frameworkSource, allProvider
41
42
 
42
43
  // Read CANONICAL (Claude) content from source, then adapt for target provider
43
44
  const canonicalContent = readFileSync(srcPath, 'utf-8');
44
- const adapted = adaptFrameworkFile(canonicalContent, file, provider);
45
+ const installedContent = applyInstalledPathReplacement(canonicalContent);
46
+ const adapted = adaptFrameworkFile(installedContent, file, provider);
45
47
 
46
48
  const destPath = join(overlayDir, file);
47
49
  mkdirSync(dirname(destPath), { recursive: true });