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.
- package/framework/config.yaml +3 -3
- package/package.json +1 -1
- package/src/config/context-file-generator.js +86 -10
- package/src/config/framework-adapter.js +2 -2
- package/src/config/gemini-hooks-generator.js +494 -0
- package/src/config/ide-configs.js +5 -1
- package/src/installer/core.js +73 -1
- package/src/installer/templates.js +145 -0
- package/src/terminal/cli-registry.js +1 -1
package/framework/config.yaml
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# chati.dev Configuration
|
|
2
|
-
version: "3.2.
|
|
2
|
+
version: "3.2.1"
|
|
3
3
|
installed_at: "2026-02-07T10:00:00Z"
|
|
4
|
-
updated_at: "2026-02-
|
|
5
|
-
installer_version: "3.2.
|
|
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
|
@@ -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/', '
|
|
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', '
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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: '.
|
|
53
|
+
localFile: '.gemini/session-lock.md',
|
|
54
54
|
},
|
|
55
55
|
codex: {
|
|
56
56
|
cliName: 'Codex CLI',
|
|
57
57
|
contextFile: 'AGENTS.md',
|
|
58
|
-
localFile: '.
|
|
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:
|
|
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
|
|
package/src/installer/core.js
CHANGED
|
@@ -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
|
+
}
|