chati-dev 3.2.0 → 3.2.1

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.
@@ -1,8 +1,8 @@
1
1
  # chati.dev Configuration
2
- version: "3.2.0"
2
+ version: "3.2.1"
3
3
  installed_at: "2026-02-07T10:00:00Z"
4
- updated_at: "2026-02-19T00:00:00Z"
5
- installer_version: "3.2.0"
4
+ updated_at: "2026-02-20T00:00:00Z"
5
+ installer_version: "3.2.1"
6
6
  project_type: greenfield
7
7
  language: en
8
8
  ides: [claude-code]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chati-dev",
3
- "version": "3.2.0",
3
+ "version": "3.2.1",
4
4
  "description": "AI-Powered Multi-Agent Orchestration System — Structured vibe coding for Full Stack Development",
5
5
  "type": "module",
6
6
  "bin": {
@@ -29,11 +29,12 @@ const GEMINI_REPLACEMENTS = [
29
29
  ['CLAUDE.md', 'GEMINI.md'],
30
30
  ['Claude.md', 'GEMINI.md'],
31
31
  ['.claude/commands/', '.gemini/commands/'],
32
- ['.claude/rules/', 'chati.dev/context/'],
32
+ ['.claude/rules/chati/', '.gemini/context/'],
33
+ ['.claude/rules/', '.gemini/context/'],
33
34
  ['.claude/mcp.json', '.gemini/settings.json'],
34
35
  ['claude --print', 'gemini --prompt'],
35
36
  ['claude -p', 'gemini --prompt'],
36
- ['CLAUDE.local.md', 'GEMINI.local.md'],
37
+ ['CLAUDE.local.md', '.gemini/session-lock.md'],
37
38
  ];
38
39
 
39
40
  /**
@@ -56,22 +57,37 @@ const CODEX_REPLACEMENTS = [
56
57
  ['CLAUDE.md', 'AGENTS.md'],
57
58
  ['Claude.md', 'AGENTS.md'],
58
59
  ['.claude/commands/', 'chati.dev/orchestrator/'],
60
+ ['.claude/rules/chati/', 'chati.dev/context/'],
59
61
  ['.claude/rules/', 'chati.dev/context/'],
60
62
  ['.claude/mcp.json', '.codex/config.toml'],
61
63
  ['claude --print', 'codex exec'],
62
64
  ['claude -p', 'codex exec'],
63
- ['CLAUDE.local.md', 'AGENTS.local.md'],
65
+ ['CLAUDE.local.md', 'AGENTS.override.md'],
64
66
  ];
65
67
 
66
68
  // ---------------------------------------------------------------------------
67
69
  // Generators
68
70
  // ---------------------------------------------------------------------------
69
71
 
72
+ /**
73
+ * @import directives appended to GEMINI.md.
74
+ * Gemini CLI resolves @import automatically, loading the full governance
75
+ * context chain + session lock — equivalent to Claude Code's rules/ + CLAUDE.local.md.
76
+ */
77
+ const GEMINI_IMPORTS = [
78
+ '@import .gemini/context/root.md',
79
+ '@import .gemini/context/governance.md',
80
+ '@import .gemini/context/protocols.md',
81
+ '@import .gemini/context/quality.md',
82
+ '@import .gemini/session-lock.md',
83
+ ];
84
+
70
85
  /**
71
86
  * Generate GEMINI.md content from CLAUDE.md content.
72
87
  *
73
88
  * Adapts the content by replacing Claude Code-specific references with
74
- * Gemini CLI equivalents. Preserves structure and formatting.
89
+ * Gemini CLI equivalents, then appends @import directives so Gemini CLI
90
+ * auto-loads governance rules and session lock (context parity with Claude).
75
91
  *
76
92
  * @param {string} content - Raw CLAUDE.md content
77
93
  * @returns {string} Adapted content for GEMINI.md
@@ -89,7 +105,10 @@ export function generateGeminiMd(content) {
89
105
  '',
90
106
  ].join('\n');
91
107
 
92
- return header + result;
108
+ // Append @import directives for context parity
109
+ const imports = '\n' + GEMINI_IMPORTS.join('\n') + '\n';
110
+
111
+ return header + result + imports;
93
112
  }
94
113
 
95
114
  /**
@@ -97,12 +116,14 @@ export function generateGeminiMd(content) {
97
116
  *
98
117
  * Simplifies the content for Codex CLI: strips hook references,
99
118
  * replaces CLI-specific paths, and produces a leaner context file
100
- * focused on code execution.
119
+ * focused on code execution. When contextFiles are provided, inlines
120
+ * governance/protocols/quality for full context parity with Claude Code.
101
121
  *
102
122
  * @param {string} content - Raw CLAUDE.md content
123
+ * @param {object} [contextFiles] - Optional inline context { root, governance, protocols, quality }
103
124
  * @returns {string} Adapted content for AGENTS.md
104
125
  */
105
- export function generateAgentsMd(content) {
126
+ export function generateAgentsMd(content, contextFiles = null) {
106
127
  let result = content;
107
128
 
108
129
  // Strip hook-related sections (Codex has no hooks support)
@@ -124,13 +145,39 @@ export function generateAgentsMd(content) {
124
145
  '',
125
146
  ].join('\n');
126
147
 
127
- return header + result;
148
+ result = header + result;
149
+
150
+ // Inline context files for full governance parity (Codex has no @import)
151
+ if (contextFiles) {
152
+ const sections = [];
153
+ for (const [name, fileContent] of Object.entries(contextFiles)) {
154
+ if (fileContent) {
155
+ // Adapt context for Codex (replace Claude-specific refs)
156
+ let adapted = fileContent;
157
+ for (const [search, replace] of CODEX_REPLACEMENTS) {
158
+ adapted = adapted.replaceAll(search, replace);
159
+ }
160
+ sections.push(adapted.trim());
161
+ }
162
+ }
163
+ if (sections.length > 0) {
164
+ result += '\n---\n\n' + sections.join('\n\n---\n\n') + '\n';
165
+ }
166
+ }
167
+
168
+ return result;
128
169
  }
129
170
 
130
171
  // ---------------------------------------------------------------------------
131
172
  // Orchestrator
132
173
  // ---------------------------------------------------------------------------
133
174
 
175
+ /**
176
+ * Context file names that provide governance parity across CLIs.
177
+ * These are read from chati.dev/context/ and inlined into AGENTS.md for Codex.
178
+ */
179
+ const CONTEXT_FILES = ['root.md', 'governance.md', 'protocols.md', 'quality.md'];
180
+
134
181
  /**
135
182
  * Generate context files for all enabled alternative providers.
136
183
  *
@@ -138,6 +185,8 @@ export function generateAgentsMd(content) {
138
185
  * CLAUDE.md from the project root, and writes the appropriate context
139
186
  * files (GEMINI.md, AGENTS.md) when their providers are active.
140
187
  *
188
+ * For Codex, governance context files are inlined into AGENTS.md to provide
189
+ * the same rules awareness that Claude gets from .claude/rules/chati/.
141
190
  *
142
191
  * @param {string} projectDir - Project root directory
143
192
  * @param {string} [baseContent] - Optional base content (used when CLAUDE.md doesn't exist on disk)
@@ -160,15 +209,18 @@ export function generateContextFiles(projectDir, baseContent = null) {
160
209
  // Determine enabled providers from config.yaml
161
210
  const enabledProviders = resolveEnabledProviders(projectDir);
162
211
 
212
+ // Read context files for inline injection (Codex)
213
+ const contextFiles = readContextFilesFromDisk(projectDir);
214
+
163
215
  // Provider-to-generator mapping
164
216
  const generators = {
165
217
  gemini: {
166
218
  filename: 'GEMINI.md',
167
- generate: generateGeminiMd,
219
+ generate: (content) => generateGeminiMd(content),
168
220
  },
169
221
  codex: {
170
222
  filename: 'AGENTS.md',
171
- generate: generateAgentsMd,
223
+ generate: (content) => generateAgentsMd(content, contextFiles),
172
224
  },
173
225
  };
174
226
 
@@ -186,6 +238,30 @@ export function generateContextFiles(projectDir, baseContent = null) {
186
238
  return result;
187
239
  }
188
240
 
241
+ /**
242
+ * Read context files from chati.dev/context/ for inline injection.
243
+ * Returns null if no context files found (backward compat).
244
+ *
245
+ * @param {string} projectDir - Project root directory
246
+ * @returns {object|null} { root, governance, protocols, quality } or null
247
+ */
248
+ function readContextFilesFromDisk(projectDir) {
249
+ const contextDir = join(projectDir, 'chati.dev', 'context');
250
+ if (!existsSync(contextDir)) return null;
251
+
252
+ const files = {};
253
+ let found = false;
254
+ for (const file of CONTEXT_FILES) {
255
+ const filePath = join(contextDir, file);
256
+ if (existsSync(filePath)) {
257
+ const key = file.replace('.md', '');
258
+ files[key] = readFileSync(filePath, 'utf-8');
259
+ found = true;
260
+ }
261
+ }
262
+ return found ? files : null;
263
+ }
264
+
189
265
  // ---------------------------------------------------------------------------
190
266
  // Helpers
191
267
  // ---------------------------------------------------------------------------
@@ -50,12 +50,12 @@ const PROVIDER_META = {
50
50
  gemini: {
51
51
  cliName: 'Gemini CLI',
52
52
  contextFile: 'GEMINI.md',
53
- localFile: '.chati/session.yaml',
53
+ localFile: '.gemini/session-lock.md',
54
54
  },
55
55
  codex: {
56
56
  cliName: 'Codex CLI',
57
57
  contextFile: 'AGENTS.md',
58
- localFile: '.chati/session.yaml',
58
+ localFile: 'AGENTS.override.md',
59
59
  },
60
60
  };
61
61
 
@@ -0,0 +1,494 @@
1
+ /**
2
+ * @fileoverview Gemini CLI hooks generator.
3
+ *
4
+ * Generates thin wrapper hooks for Gemini CLI that import shared logic
5
+ * from chati.dev/hooks/. Each hook translates between Gemini CLI event
6
+ * format and the existing hook logic, providing governance parity with
7
+ * Claude Code.
8
+ *
9
+ * Constitution Article XIX — hooks are generated at install time, zero runtime overhead.
10
+ *
11
+ * Gemini CLI Hook Events (used by chati.dev):
12
+ * - BeforeModel: runs before model inference (PRISM injection, model advisory)
13
+ * - BeforeTool: runs before tool execution (mode governance, constitution guard, read protection)
14
+ * - PreCompress: runs before context compression (session digest)
15
+ */
16
+
17
+ // ---------------------------------------------------------------------------
18
+ // Hook Map — Claude Hook → Gemini Event
19
+ // ---------------------------------------------------------------------------
20
+
21
+ /**
22
+ * Maps each chati.dev hook to its Gemini CLI equivalent event.
23
+ */
24
+ export const HOOK_MAP = {
25
+ 'prism-engine': { event: 'BeforeModel', description: 'Inject PRISM context into model prompt' },
26
+ 'model-governance': { event: 'BeforeModel', description: 'Advisory: recommended model per agent' },
27
+ 'mode-governance': { event: 'BeforeTool', description: 'Block writes outside current mode scope' },
28
+ 'constitution-guard': { event: 'BeforeTool', description: 'Block destructive commands and secret writes' },
29
+ 'read-protection': { event: 'BeforeTool', description: 'Block reads of sensitive files' },
30
+ 'session-digest': { event: 'PreCompress', description: 'Save session state before context compression' },
31
+ };
32
+
33
+ // ---------------------------------------------------------------------------
34
+ // Hook Templates
35
+ // ---------------------------------------------------------------------------
36
+
37
+ /**
38
+ * Shared header for all generated hooks.
39
+ */
40
+ const HOOK_HEADER = `#!/usr/bin/env node
41
+ /**
42
+ * Auto-generated by chati.dev — Gemini CLI hook wrapper.
43
+ * Imports shared logic from chati.dev/hooks/ for governance parity.
44
+ * Do not edit manually — regenerate with \`npx chati-dev init\`.
45
+ */
46
+
47
+ import { existsSync, readFileSync } from 'fs';
48
+ import { join } from 'path';
49
+ import { fileURLToPath } from 'url';
50
+
51
+ const __filename = fileURLToPath(import.meta.url);
52
+ const __dirname = join(__filename, '..');
53
+ const projectRoot = join(__dirname, '..', '..');
54
+ `;
55
+
56
+ /**
57
+ * Generate the PRISM engine hook for Gemini CLI.
58
+ * BeforeModel event — injects PRISM context XML into the model prompt.
59
+ */
60
+ function generatePrismEngine() {
61
+ return `${HOOK_HEADER}
62
+ /**
63
+ * PRISM Engine — BeforeModel
64
+ * Reads session state and injects PRISM context block.
65
+ */
66
+ async function main() {
67
+ let input = '';
68
+ for await (const chunk of process.stdin) {
69
+ input += chunk;
70
+ }
71
+
72
+ try {
73
+ const event = JSON.parse(input);
74
+ const cwd = event.cwd || process.cwd();
75
+
76
+ const sessionPath = join(cwd, '.chati', 'session.yaml');
77
+ if (!existsSync(sessionPath)) {
78
+ console.log(JSON.stringify({}));
79
+ return;
80
+ }
81
+
82
+ // Delegate to shared PRISM engine logic
83
+ const hookPath = join(cwd, 'chati.dev', 'hooks', 'prism-engine.js');
84
+ if (existsSync(hookPath)) {
85
+ const mod = await import(hookPath);
86
+ if (typeof mod.buildPrismContext === 'function') {
87
+ const context = await mod.buildPrismContext(cwd);
88
+ if (context) {
89
+ console.log(JSON.stringify({ additionalContext: context }));
90
+ return;
91
+ }
92
+ }
93
+ }
94
+
95
+ console.log(JSON.stringify({}));
96
+ } catch {
97
+ console.log(JSON.stringify({}));
98
+ }
99
+ }
100
+
101
+ main();
102
+ `;
103
+ }
104
+
105
+ /**
106
+ * Generate the model governance hook for Gemini CLI.
107
+ * BeforeModel event — advisory about recommended model per agent.
108
+ */
109
+ function generateModelGovernance() {
110
+ return `${HOOK_HEADER}
111
+ /**
112
+ * Model Governance — BeforeModel
113
+ * Advisory: logs recommended model for current agent.
114
+ */
115
+ async function main() {
116
+ let input = '';
117
+ for await (const chunk of process.stdin) {
118
+ input += chunk;
119
+ }
120
+
121
+ try {
122
+ const event = JSON.parse(input);
123
+ const cwd = event.cwd || process.cwd();
124
+
125
+ const sessionPath = join(cwd, '.chati', 'session.yaml');
126
+ if (!existsSync(sessionPath)) {
127
+ console.log(JSON.stringify({}));
128
+ return;
129
+ }
130
+
131
+ // Read current agent from session
132
+ const raw = readFileSync(sessionPath, 'utf-8');
133
+ const agentMatch = raw.match(/^\\s*current_agent:\\s*(.+)$/m);
134
+ const agent = agentMatch ? agentMatch[1].trim().replace(/^["']|["']$/g, '') : null;
135
+
136
+ if (agent) {
137
+ // Delegate to shared model governance logic
138
+ const hookPath = join(cwd, 'chati.dev', 'hooks', 'model-governance.js');
139
+ if (existsSync(hookPath)) {
140
+ const mod = await import(hookPath);
141
+ if (typeof mod.getRecommendedModel === 'function') {
142
+ const recommendation = mod.getRecommendedModel(cwd, agent);
143
+ if (recommendation) {
144
+ console.log(JSON.stringify({ advisory: \`Recommended model: \${recommendation}\` }));
145
+ return;
146
+ }
147
+ }
148
+ }
149
+ }
150
+
151
+ console.log(JSON.stringify({}));
152
+ } catch {
153
+ console.log(JSON.stringify({}));
154
+ }
155
+ }
156
+
157
+ main();
158
+ `;
159
+ }
160
+
161
+ /**
162
+ * Generate the mode governance hook for Gemini CLI.
163
+ * BeforeTool event — blocks writes outside the scope of current governance mode.
164
+ */
165
+ function generateModeGovernance() {
166
+ return `${HOOK_HEADER}
167
+ /**
168
+ * Mode Governance — BeforeTool
169
+ * Blocks write operations outside current mode scope.
170
+ * Constitution Article XI enforcement.
171
+ */
172
+ const MODE_SCOPES = {
173
+ planning: { allowed: ['chati.dev/', '.chati/', 'chati.dev/artifacts/'] },
174
+ build: { allowed: ['*'] },
175
+ deploy: { allowed: ['*'] },
176
+ };
177
+
178
+ const STATE_TO_MODE = {
179
+ discover: 'planning', plan: 'planning', planning: 'planning',
180
+ build: 'build', validate: 'build',
181
+ deploy: 'deploy', completed: 'deploy',
182
+ };
183
+
184
+ function getCurrentMode(cwd) {
185
+ const sessionPath = join(cwd, '.chati', 'session.yaml');
186
+ if (!existsSync(sessionPath)) return 'planning';
187
+ const raw = readFileSync(sessionPath, 'utf-8');
188
+ const match = raw.match(/^\\s*state:\\s*(.+)$/m);
189
+ const state = match ? match[1].trim().replace(/^["']|["']$/g, '') : 'discover';
190
+ return STATE_TO_MODE[state] || 'planning';
191
+ }
192
+
193
+ function isPathAllowed(filePath, cwd, mode) {
194
+ const scope = MODE_SCOPES[mode];
195
+ if (!scope) return false;
196
+ if (scope.allowed.includes('*')) return true;
197
+
198
+ // Normalize to relative path
199
+ const rel = filePath.startsWith('/') ? filePath.slice(cwd.length + 1) : filePath;
200
+
201
+ // Check path escape
202
+ if (rel.startsWith('..') || rel.startsWith('/')) return false;
203
+
204
+ return scope.allowed.some(prefix => rel.startsWith(prefix));
205
+ }
206
+
207
+ async function main() {
208
+ let input = '';
209
+ for await (const chunk of process.stdin) {
210
+ input += chunk;
211
+ }
212
+
213
+ try {
214
+ const event = JSON.parse(input);
215
+ const cwd = event.cwd || process.cwd();
216
+ const toolName = event.tool_name || '';
217
+ const toolInput = event.tool_input || {};
218
+
219
+ // Only intercept write/edit operations
220
+ if (!['WriteFile', 'EditFile', 'Write', 'Edit'].includes(toolName)) {
221
+ console.log(JSON.stringify({}));
222
+ return;
223
+ }
224
+
225
+ const filePath = toolInput.path || toolInput.file_path || '';
226
+ if (!filePath) {
227
+ console.log(JSON.stringify({}));
228
+ return;
229
+ }
230
+
231
+ const mode = getCurrentMode(cwd);
232
+ if (isPathAllowed(filePath, cwd, mode)) {
233
+ console.log(JSON.stringify({}));
234
+ } else {
235
+ console.log(JSON.stringify({
236
+ error: \`[Article XI] Cannot write "\${filePath}" in \${mode} mode. Allowed: \${MODE_SCOPES[mode].allowed.join(', ')}\`
237
+ }));
238
+ }
239
+ } catch {
240
+ console.log(JSON.stringify({}));
241
+ }
242
+ }
243
+
244
+ main();
245
+ `;
246
+ }
247
+
248
+ /**
249
+ * Generate the constitution guard hook for Gemini CLI.
250
+ * BeforeTool event — blocks destructive commands and secret writes.
251
+ */
252
+ function generateConstitutionGuard() {
253
+ return `${HOOK_HEADER}
254
+ /**
255
+ * Constitution Guard — BeforeTool
256
+ * Blocks destructive commands and secret writes.
257
+ * Constitution Article XI enforcement.
258
+ */
259
+
260
+ const DESTRUCTIVE_PATTERNS = [
261
+ /rm\\s+-rf\\s+\\//,
262
+ /git\\s+reset\\s+--hard/,
263
+ /git\\s+push\\s+--force/,
264
+ /git\\s+push\\s+-f/,
265
+ /git\\s+clean\\s+-fd/,
266
+ /DROP\\s+TABLE/i,
267
+ /DROP\\s+DATABASE/i,
268
+ /TRUNCATE\\s+TABLE/i,
269
+ /chmod\\s+777/,
270
+ ];
271
+
272
+ const SECRET_PATTERNS = [
273
+ /API_KEY\\s*=/,
274
+ /SECRET_KEY\\s*=/,
275
+ /PASSWORD\\s*=/,
276
+ /PRIVATE_KEY\\s*=/,
277
+ /aws_secret_access_key/i,
278
+ /-----BEGIN.*PRIVATE KEY-----/,
279
+ ];
280
+
281
+ const PROTECTED_WRITE_FILES = ['.env', '.env.local', '.env.production', 'credentials.json', 'service-account.json'];
282
+ const SAFE_PATTERNS = ['.env.example', '.env.template', '.env.sample'];
283
+
284
+ async function main() {
285
+ let input = '';
286
+ for await (const chunk of process.stdin) {
287
+ input += chunk;
288
+ }
289
+
290
+ try {
291
+ const event = JSON.parse(input);
292
+ const toolName = event.tool_name || '';
293
+ const toolInput = event.tool_input || {};
294
+
295
+ // Check Bash commands for destructive patterns
296
+ if (['Bash', 'RunCommand'].includes(toolName)) {
297
+ const cmd = toolInput.command || '';
298
+ for (const pattern of DESTRUCTIVE_PATTERNS) {
299
+ if (pattern.test(cmd)) {
300
+ console.log(JSON.stringify({
301
+ error: \`[Constitution Guard] Blocked destructive command: \${cmd.slice(0, 80)}\`
302
+ }));
303
+ return;
304
+ }
305
+ }
306
+ }
307
+
308
+ // Check Write/Edit for secret patterns and protected files
309
+ if (['WriteFile', 'EditFile', 'Write', 'Edit'].includes(toolName)) {
310
+ const filePath = toolInput.path || toolInput.file_path || '';
311
+ const content = toolInput.content || toolInput.new_string || '';
312
+
313
+ // Check if writing to a protected file
314
+ if (PROTECTED_WRITE_FILES.some(p => filePath.endsWith(p)) &&
315
+ !SAFE_PATTERNS.some(s => filePath.endsWith(s))) {
316
+ console.log(JSON.stringify({
317
+ error: \`[Constitution Guard] Cannot write to protected file: \${filePath}\`
318
+ }));
319
+ return;
320
+ }
321
+
322
+ // Check content for secret patterns
323
+ for (const pattern of SECRET_PATTERNS) {
324
+ if (pattern.test(content)) {
325
+ console.log(JSON.stringify({
326
+ error: '[Constitution Guard] Blocked write containing secrets/credentials'
327
+ }));
328
+ return;
329
+ }
330
+ }
331
+ }
332
+
333
+ console.log(JSON.stringify({}));
334
+ } catch {
335
+ console.log(JSON.stringify({}));
336
+ }
337
+ }
338
+
339
+ main();
340
+ `;
341
+ }
342
+
343
+ /**
344
+ * Generate the read protection hook for Gemini CLI.
345
+ * BeforeTool event — blocks reads of sensitive files.
346
+ */
347
+ function generateReadProtection() {
348
+ return `${HOOK_HEADER}
349
+ /**
350
+ * Read Protection — BeforeTool
351
+ * Blocks reading sensitive files (.env, .pem, credentials).
352
+ * Constitution Article XI enforcement.
353
+ */
354
+
355
+ const PROTECTED_FILES = ['.env', '.env.local', '.env.production', '.env.staging'];
356
+ const PROTECTED_EXTENSIONS = ['.pem', '.key', '.p12', '.pfx', '.jks'];
357
+ const PROTECTED_PATHS = ['credentials.json', 'service-account.json', '.git/config', '.aws/credentials', '.npmrc'];
358
+ const SAFE_FILES = ['.env.example', '.env.template', '.env.sample'];
359
+
360
+ async function main() {
361
+ let input = '';
362
+ for await (const chunk of process.stdin) {
363
+ input += chunk;
364
+ }
365
+
366
+ try {
367
+ const event = JSON.parse(input);
368
+ const toolName = event.tool_name || '';
369
+ const toolInput = event.tool_input || {};
370
+
371
+ // Only intercept read operations
372
+ if (!['ReadFile', 'Read'].includes(toolName)) {
373
+ console.log(JSON.stringify({}));
374
+ return;
375
+ }
376
+
377
+ const filePath = toolInput.path || toolInput.file_path || '';
378
+ if (!filePath) {
379
+ console.log(JSON.stringify({}));
380
+ return;
381
+ }
382
+
383
+ // Allow safe patterns first
384
+ if (SAFE_FILES.some(s => filePath.endsWith(s))) {
385
+ console.log(JSON.stringify({}));
386
+ return;
387
+ }
388
+
389
+ // Block protected files
390
+ if (PROTECTED_FILES.some(p => filePath.endsWith(p))) {
391
+ console.log(JSON.stringify({
392
+ error: \`[Read Protection] Cannot read protected file: \${filePath}\`
393
+ }));
394
+ return;
395
+ }
396
+
397
+ // Block protected extensions
398
+ if (PROTECTED_EXTENSIONS.some(ext => filePath.endsWith(ext))) {
399
+ console.log(JSON.stringify({
400
+ error: \`[Read Protection] Cannot read sensitive file type: \${filePath}\`
401
+ }));
402
+ return;
403
+ }
404
+
405
+ // Block protected paths
406
+ if (PROTECTED_PATHS.some(p => filePath.includes(p))) {
407
+ console.log(JSON.stringify({
408
+ error: \`[Read Protection] Cannot read protected path: \${filePath}\`
409
+ }));
410
+ return;
411
+ }
412
+
413
+ console.log(JSON.stringify({}));
414
+ } catch {
415
+ console.log(JSON.stringify({}));
416
+ }
417
+ }
418
+
419
+ main();
420
+ `;
421
+ }
422
+
423
+ /**
424
+ * Generate the session digest hook for Gemini CLI.
425
+ * PreCompress event — saves session state before context compression.
426
+ */
427
+ function generateSessionDigest() {
428
+ return `${HOOK_HEADER}
429
+ /**
430
+ * Session Digest — PreCompress
431
+ * Saves session state before context compression.
432
+ */
433
+ async function main() {
434
+ let input = '';
435
+ for await (const chunk of process.stdin) {
436
+ input += chunk;
437
+ }
438
+
439
+ try {
440
+ const event = JSON.parse(input);
441
+ const cwd = event.cwd || process.cwd();
442
+
443
+ // Delegate to shared session digest logic
444
+ const hookPath = join(cwd, 'chati.dev', 'hooks', 'session-digest.js');
445
+ if (existsSync(hookPath)) {
446
+ const mod = await import(hookPath);
447
+ if (typeof mod.saveDigest === 'function') {
448
+ await mod.saveDigest(cwd);
449
+ }
450
+ }
451
+
452
+ console.log(JSON.stringify({}));
453
+ } catch {
454
+ console.log(JSON.stringify({}));
455
+ }
456
+ }
457
+
458
+ main();
459
+ `;
460
+ }
461
+
462
+ // ---------------------------------------------------------------------------
463
+ // Public API
464
+ // ---------------------------------------------------------------------------
465
+
466
+ /**
467
+ * Generate all 6 Gemini CLI hook files.
468
+ *
469
+ * @returns {Record<string, string>} Map of filename → file content
470
+ */
471
+ export function generateAllGeminiHooks() {
472
+ return {
473
+ 'prism-engine.js': generatePrismEngine(),
474
+ 'model-governance.js': generateModelGovernance(),
475
+ 'mode-governance.js': generateModeGovernance(),
476
+ 'constitution-guard.js': generateConstitutionGuard(),
477
+ 'read-protection.js': generateReadProtection(),
478
+ 'session-digest.js': generateSessionDigest(),
479
+ };
480
+ }
481
+
482
+ /**
483
+ * Generate .gemini/settings.json content with hook configuration.
484
+ *
485
+ * @returns {string} JSON string for .gemini/settings.json
486
+ */
487
+ export function generateGeminiSettings() {
488
+ const hooks = Object.entries(HOOK_MAP).map(([name, config]) => ({
489
+ path: `.gemini/hooks/${name}.js`,
490
+ event: config.event,
491
+ }));
492
+
493
+ return JSON.stringify({ hooks }, null, 2) + '\n';
494
+ }
@@ -47,15 +47,19 @@ export const IDE_CONFIGS = {
47
47
  rulesFile: 'AGENTS.md',
48
48
  mcpConfigFile: null,
49
49
  formatNotes: 'Codex skill format (SKILL.md with YAML frontmatter)',
50
+ rulesPath: '.codex/rules/',
51
+ overrideFile: 'AGENTS.override.md',
50
52
  },
51
53
  'gemini-cli': {
52
54
  name: 'Gemini CLI',
53
55
  description: 'Google AI terminal agent',
54
56
  recommended: false,
55
57
  configPath: '.gemini/commands/',
56
- rulesFile: null,
58
+ rulesFile: 'GEMINI.md',
57
59
  mcpConfigFile: '.gemini/settings.json',
58
60
  formatNotes: 'TOML command format',
61
+ contextPath: '.gemini/context/',
62
+ hooksPath: '.gemini/hooks/',
59
63
  },
60
64
  };
61
65
 
@@ -3,7 +3,7 @@ import { join, dirname } from 'path';
3
3
  import { fileURLToPath } from 'url';
4
4
  import { IDE_CONFIGS } from '../config/ide-configs.js';
5
5
  import { generateClaudeMCPConfig } from '../config/mcp-configs.js';
6
- import { generateSessionYaml, generateConfigYaml, generateClaudeMd, generateClaudeLocalMd, generateCodexSkill, generateGeminiRouter } from './templates.js';
6
+ import { generateSessionYaml, generateConfigYaml, generateClaudeMd, generateClaudeLocalMd, generateCodexSkill, generateGeminiRouter, generateGeminiSessionLock, generateAgentsOverrideMd, generateCodexConstitutionGuardRules, generateCodexReadProtectionRules } from './templates.js';
7
7
  import { generateContextFiles } from '../config/context-file-generator.js';
8
8
  import { adaptFrameworkFile, ADAPTABLE_FILES } from '../config/framework-adapter.js';
9
9
  import { verifyManifest } from './manifest.js';
@@ -128,6 +128,9 @@ export async function installFramework(config) {
128
128
  if (hasNonClaude) {
129
129
  generateContextFiles(targetDir, baseContent);
130
130
  }
131
+
132
+ // 7. Update .gitignore with runtime session lock files
133
+ updateGitignore(targetDir, selectedIDEs);
131
134
  }
132
135
 
133
136
  /**
@@ -314,9 +317,42 @@ Pass through all context: session state, handoffs, artifacts, and user input.
314
317
  // Codex CLI: chati skill via .agents/skills/chati/SKILL.md (invoke with $chati)
315
318
  createDir(join(targetDir, '.agents', 'skills', 'chati'));
316
319
  writeFileSync(join(targetDir, '.agents', 'skills', 'chati', 'SKILL.md'), generateCodexSkill(), 'utf-8');
320
+
321
+ // Session lock override file (equivalent to CLAUDE.local.md)
322
+ writeFileSync(join(targetDir, 'AGENTS.override.md'), generateAgentsOverrideMd(), 'utf-8');
323
+
324
+ // Starlark execution policies (equivalent to Claude hooks for constitution guard + read protection)
325
+ createDir(join(targetDir, '.codex', 'rules'));
326
+ writeFileSync(join(targetDir, '.codex', 'rules', 'constitution-guard.rules'), generateCodexConstitutionGuardRules(), 'utf-8');
327
+ writeFileSync(join(targetDir, '.codex', 'rules', 'read-protection.rules'), generateCodexReadProtectionRules(), 'utf-8');
317
328
  } else if (ideKey === 'gemini-cli') {
318
329
  // Gemini CLI: TOML command file (native format for /chati command)
319
330
  writeFileSync(join(targetDir, '.gemini', 'commands', 'chati.toml'), generateGeminiRouter(), 'utf-8');
331
+
332
+ // Context files via @import (equivalent to .claude/rules/chati/)
333
+ const geminiContextDir = join(targetDir, '.gemini', 'context');
334
+ createDir(geminiContextDir);
335
+ const contextFileNames = ['root.md', 'governance.md', 'protocols.md', 'quality.md'];
336
+ for (const file of contextFileNames) {
337
+ const src = join(FRAMEWORK_SOURCE, 'context', file);
338
+ if (existsSync(src)) {
339
+ const content = readFileSync(src, 'utf-8');
340
+ writeFileSync(join(geminiContextDir, file), adaptFrameworkFile(content, `context/${file}`, 'gemini'), 'utf-8');
341
+ }
342
+ }
343
+
344
+ // Session lock (equivalent to CLAUDE.local.md, imported via @import in GEMINI.md)
345
+ writeFileSync(join(targetDir, '.gemini', 'session-lock.md'), generateGeminiSessionLock(), 'utf-8');
346
+
347
+ // Hooks (6 governance hooks — equivalent to Claude Code hooks)
348
+ const geminiHooksDir = join(targetDir, '.gemini', 'hooks');
349
+ createDir(geminiHooksDir);
350
+ const { generateAllGeminiHooks, generateGeminiSettings } = await import('../config/gemini-hooks-generator.js');
351
+ const hooks = generateAllGeminiHooks();
352
+ for (const [filename, hookContent] of Object.entries(hooks)) {
353
+ writeFileSync(join(geminiHooksDir, filename), hookContent, 'utf-8');
354
+ }
355
+ writeFileSync(join(targetDir, '.gemini', 'settings.json'), generateGeminiSettings(), 'utf-8');
320
356
  } else {
321
357
  // VS Code, Cursor, AntiGravity — generic rules file
322
358
  if (config.rulesFile) {
@@ -364,6 +400,42 @@ DEPLOY: DevOps
364
400
  `;
365
401
  }
366
402
 
403
+ /**
404
+ * Append chati.dev runtime entries to .gitignore.
405
+ * Session lock files are runtime-only and should never be committed.
406
+ */
407
+ function updateGitignore(targetDir, selectedIDEs) {
408
+ const entries = [
409
+ '',
410
+ '# Chati.dev runtime files (session lock — not committed)',
411
+ '.chati/memories/*/session/',
412
+ ];
413
+
414
+ if (selectedIDEs.includes('claude-code')) {
415
+ entries.push('CLAUDE.local.md');
416
+ }
417
+ if (selectedIDEs.includes('gemini-cli')) {
418
+ entries.push('.gemini/session-lock.md');
419
+ }
420
+ if (selectedIDEs.includes('codex-cli')) {
421
+ entries.push('AGENTS.override.md');
422
+ }
423
+
424
+ entries.push('');
425
+
426
+ const gitignorePath = join(targetDir, '.gitignore');
427
+ const marker = '# Chati.dev runtime files';
428
+
429
+ if (existsSync(gitignorePath)) {
430
+ const existing = readFileSync(gitignorePath, 'utf-8');
431
+ // Don't duplicate if already present
432
+ if (existing.includes(marker)) return;
433
+ writeFileSync(gitignorePath, existing.trimEnd() + '\n' + entries.join('\n'), 'utf-8');
434
+ } else {
435
+ writeFileSync(gitignorePath, entries.join('\n'), 'utf-8');
436
+ }
437
+ }
438
+
367
439
  /**
368
440
  * Recursively create directory if it doesn't exist
369
441
  */
@@ -243,3 +243,148 @@ _No decisions yet. Start with /chati._
243
243
  _Auto-updated by Chati.dev orchestrator_
244
244
  `;
245
245
  }
246
+
247
+ /**
248
+ * Generate Gemini CLI session lock (.gemini/session-lock.md)
249
+ * Equivalent to CLAUDE.local.md — imported via @import in GEMINI.md.
250
+ */
251
+ export function generateGeminiSessionLock() {
252
+ return `# Chati.dev Runtime State
253
+
254
+ ## Session Lock
255
+ **Status: INACTIVE** — Type \`/chati\` to activate.
256
+
257
+ <!-- SESSION-LOCK:INACTIVE -->
258
+
259
+ ## Current State
260
+ - **Agent**: None (ready to start)
261
+ - **Pipeline**: Pre-start
262
+ - **Mode**: interactive
263
+
264
+ ## Recent Decisions
265
+ _No decisions yet. Start with /chati._
266
+
267
+ ---
268
+ _Auto-updated by Chati.dev orchestrator_
269
+ `;
270
+ }
271
+
272
+ /**
273
+ * Generate Codex CLI session lock (AGENTS.override.md)
274
+ * Codex auto-loads this as an override to AGENTS.md.
275
+ */
276
+ export function generateAgentsOverrideMd() {
277
+ return `# Chati.dev Runtime State
278
+
279
+ ## Session Lock
280
+ **Status: INACTIVE** — Type \`$chati\` to activate.
281
+
282
+ <!-- SESSION-LOCK:INACTIVE -->
283
+
284
+ ## Current State
285
+ - **Agent**: None (ready to start)
286
+ - **Pipeline**: Pre-start
287
+ - **Mode**: interactive
288
+
289
+ ## Recent Decisions
290
+ _No decisions yet. Start with $chati._
291
+
292
+ ---
293
+ _Auto-updated by Chati.dev orchestrator_
294
+ `;
295
+ }
296
+
297
+ /**
298
+ * Generate Codex CLI constitution guard rules (.codex/rules/constitution-guard.rules)
299
+ * Starlark execution policy that blocks destructive commands and secret writes.
300
+ */
301
+ export function generateCodexConstitutionGuardRules() {
302
+ return `# Chati.dev Constitution Guard — Codex CLI Execution Policy
303
+ # Article XI: Block destructive commands and secret writes
304
+
305
+ # Destructive commands that require explicit user approval
306
+ deny_commands = [
307
+ "rm -rf",
308
+ "git reset --hard",
309
+ "git push --force",
310
+ "git push -f",
311
+ "git clean -fd",
312
+ "DROP TABLE",
313
+ "DROP DATABASE",
314
+ "TRUNCATE TABLE",
315
+ "chmod 777",
316
+ ]
317
+
318
+ # Patterns that indicate secret/credential writes
319
+ deny_write_patterns = [
320
+ "API_KEY=",
321
+ "SECRET_KEY=",
322
+ "PASSWORD=",
323
+ "TOKEN=",
324
+ "PRIVATE_KEY=",
325
+ "aws_secret_access_key",
326
+ "-----BEGIN RSA PRIVATE KEY-----",
327
+ "-----BEGIN OPENSSH PRIVATE KEY-----",
328
+ ]
329
+
330
+ # Files that should never be written to
331
+ deny_write_files = [
332
+ ".env",
333
+ ".env.local",
334
+ ".env.production",
335
+ "credentials.json",
336
+ "service-account.json",
337
+ ]
338
+
339
+ # Allow list — these are safe even though they match patterns
340
+ allow_write_files = [
341
+ ".env.example",
342
+ ".env.template",
343
+ ".env.sample",
344
+ ]
345
+ `;
346
+ }
347
+
348
+ /**
349
+ * Generate Codex CLI read protection rules (.codex/rules/read-protection.rules)
350
+ * Starlark execution policy that blocks reading sensitive files.
351
+ */
352
+ export function generateCodexReadProtectionRules() {
353
+ return `# Chati.dev Read Protection — Codex CLI Execution Policy
354
+ # Article XI: Protect sensitive files from being read
355
+
356
+ # Files that should never be read
357
+ deny_read_files = [
358
+ ".env",
359
+ ".env.local",
360
+ ".env.production",
361
+ ".env.staging",
362
+ ]
363
+
364
+ # File extensions that indicate sensitive content
365
+ deny_read_extensions = [
366
+ ".pem",
367
+ ".key",
368
+ ".p12",
369
+ ".pfx",
370
+ ".jks",
371
+ ]
372
+
373
+ # Paths that should never be read
374
+ deny_read_paths = [
375
+ "credentials.json",
376
+ "service-account.json",
377
+ ".git/config",
378
+ ".ssh/",
379
+ ".aws/credentials",
380
+ ".npmrc",
381
+ ]
382
+
383
+ # Allow list — these are safe even though they match patterns
384
+ allow_read_files = [
385
+ ".env.example",
386
+ ".env.template",
387
+ ".env.sample",
388
+ ]
389
+ `;
390
+ }
@@ -51,7 +51,7 @@ const PROVIDERS = {
51
51
  baseArgs: [],
52
52
  modelFlag: '--model',
53
53
  stdinSupport: true,
54
- hooksSupport: false,
54
+ hooksSupport: true,
55
55
  mcpSupport: true,
56
56
  contextFile: 'GEMINI.md',
57
57
  modelMap: {