chati-dev 3.2.0 → 3.2.2
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/README.md +57 -26
- package/framework/config.yaml +3 -3
- package/package.json +1 -1
- package/src/config/context-file-generator.js +87 -10
- package/src/config/framework-adapter.js +7 -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/README.md
CHANGED
|
@@ -80,12 +80,16 @@ The wizard guides you through 4 steps:
|
|
|
80
80
|
|
|
81
81
|
Open your AI CLI in the same project directory, then type:
|
|
82
82
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
83
|
+
| CLI | Command | Notes |
|
|
84
|
+
|-----|---------|-------|
|
|
85
|
+
| **Claude Code** | `/chati` | Slash command (native) |
|
|
86
|
+
| **Gemini CLI** | `/chati` | TOML custom command |
|
|
87
|
+
| **Codex CLI** | `$chati` | Skill invocation (Codex uses `$` for skills, not `/`) |
|
|
86
88
|
|
|
87
89
|
The orchestrator loads your session, detects where you left off, and routes you to the right agent. You stay inside the system until you explicitly exit.
|
|
88
90
|
|
|
91
|
+
> **Codex CLI note:** You can also just describe what you want (e.g., "start chati" or "plan my project") and Codex will auto-invoke the skill based on its description.
|
|
92
|
+
|
|
89
93
|
### 3. Follow the pipeline
|
|
90
94
|
|
|
91
95
|
The agents guide you through each phase:
|
|
@@ -108,10 +112,13 @@ npx chati-dev status --watch # Auto-refresh every 5s
|
|
|
108
112
|
|
|
109
113
|
### Exit & Resume
|
|
110
114
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
+
| CLI | Exit | Resume |
|
|
116
|
+
|-----|------|--------|
|
|
117
|
+
| **Claude Code** | `/chati exit` | `/chati` |
|
|
118
|
+
| **Gemini CLI** | `/chati exit` | `/chati` |
|
|
119
|
+
| **Codex CLI** | `$chati exit` | `$chati` |
|
|
120
|
+
|
|
121
|
+
The system saves your full session state — pipeline position, current agent, decisions, and context. You resume exactly where you left off, even across days or machines.
|
|
115
122
|
|
|
116
123
|
---
|
|
117
124
|
|
|
@@ -127,7 +134,7 @@ npx chati-dev status --watch # Auto-refresh every 5s
|
|
|
127
134
|
| **Multi-Terminal** | Autonomous agents run in parallel in separate terminals. Detail, Architect, and UX agents work simultaneously |
|
|
128
135
|
| **Memory System** | The system learns from mistakes. Gotchas are captured automatically and recalled when relevant |
|
|
129
136
|
| **Execution Profiles** | Three profiles — explore (read-only), guided (default), autonomous (gate >= 90%) — with safety net and circuit breaker |
|
|
130
|
-
| **IDE-Agnostic** | Works with Claude Code, VS Code, Cursor, Gemini CLI, and AntiGravity |
|
|
137
|
+
| **IDE-Agnostic** | Works with Claude Code, VS Code, Cursor, Gemini CLI, Codex CLI, and AntiGravity |
|
|
131
138
|
| **4 Languages** | Interface supports English, Portuguese, Spanish, and French. Artifacts are always generated in English |
|
|
132
139
|
| **Supply Chain Security** | Every file is cryptographically signed (Ed25519). Tampered packages are blocked on install |
|
|
133
140
|
|
|
@@ -203,7 +210,7 @@ The system starts in `guided` mode. Transition to `autonomous` requires both QA
|
|
|
203
210
|
### How the Pipeline Works
|
|
204
211
|
|
|
205
212
|
```
|
|
206
|
-
You
|
|
213
|
+
You activate the orchestrator (/chati or $chati)
|
|
207
214
|
│
|
|
208
215
|
▼
|
|
209
216
|
┌──────────────────────────────────────────────┐
|
|
@@ -280,13 +287,14 @@ The system is governed by a **19-article Constitution** that enforces agent beha
|
|
|
280
287
|
|
|
281
288
|
## Supported IDEs
|
|
282
289
|
|
|
283
|
-
| IDE | How it connects |
|
|
284
|
-
|
|
285
|
-
| **Claude Code** | `.claude/commands/chati.md` → orchestrator |
|
|
286
|
-
| **
|
|
287
|
-
| **
|
|
288
|
-
| **
|
|
289
|
-
| **
|
|
290
|
+
| IDE | How it connects | Activation |
|
|
291
|
+
|-----|----------------|------------|
|
|
292
|
+
| **Claude Code** | `.claude/commands/chati.md` → orchestrator | `/chati` |
|
|
293
|
+
| **Gemini CLI** | `.gemini/commands/chati.toml` → orchestrator | `/chati` |
|
|
294
|
+
| **Codex CLI** | `.agents/skills/chati/SKILL.md` → orchestrator | `$chati` |
|
|
295
|
+
| **VS Code** | `.vscode/chati.md` → orchestrator | Extension-dependent |
|
|
296
|
+
| **Cursor** | `.cursor/rules/chati.md` → orchestrator | Extension-dependent |
|
|
297
|
+
| **AntiGravity** | Platform agent config → orchestrator | Platform-dependent |
|
|
290
298
|
|
|
291
299
|
All IDEs use a thin router file that points to the same orchestrator. Your project works the same regardless of which IDE you use.
|
|
292
300
|
|
|
@@ -323,23 +331,27 @@ All IDEs use a thin router file that points to the same orchestrator. Your proje
|
|
|
323
331
|
|
|
324
332
|
### Inside an Active Session
|
|
325
333
|
|
|
326
|
-
|
|
|
327
|
-
|
|
328
|
-
|
|
|
329
|
-
| `/chati status` |
|
|
330
|
-
| `/chati help` |
|
|
331
|
-
| `/chati resume` |
|
|
332
|
-
| `/chati exit` |
|
|
334
|
+
| Action | Claude Code / Gemini | Codex CLI |
|
|
335
|
+
|--------|---------------------|-----------|
|
|
336
|
+
| Start or resume session | `/chati` | `$chati` |
|
|
337
|
+
| Show pipeline progress | `/chati status` | `$chati status` |
|
|
338
|
+
| Show available commands | `/chati help` | `$chati help` |
|
|
339
|
+
| Resume from continuation | `/chati resume` | `$chati resume` |
|
|
340
|
+
| Save session and exit | `/chati exit` | `$chati exit` |
|
|
333
341
|
|
|
334
342
|
---
|
|
335
343
|
|
|
336
344
|
## Project Structure
|
|
337
345
|
|
|
346
|
+
The structure below shows the Claude Code layout (default). Gemini and Codex get equivalent files in their native locations.
|
|
347
|
+
|
|
338
348
|
```
|
|
339
349
|
your-project/
|
|
340
350
|
├── .chati/
|
|
341
351
|
│ ├── session.yaml # Session state (auto-managed, gitignored)
|
|
342
352
|
│ └── memories/ # Memory storage (gitignored)
|
|
353
|
+
│
|
|
354
|
+
│── # ─── Claude Code ───────────────────
|
|
343
355
|
├── .claude/
|
|
344
356
|
│ ├── commands/
|
|
345
357
|
│ │ └── chati.md # Thin router → orchestrator
|
|
@@ -350,7 +362,26 @@ your-project/
|
|
|
350
362
|
│ ├── protocols.md # Universal protocols
|
|
351
363
|
│ └── quality.md # Quality standards
|
|
352
364
|
├── CLAUDE.md # Project context (auto-generated)
|
|
353
|
-
├── CLAUDE.local.md # Runtime state (gitignored)
|
|
365
|
+
├── CLAUDE.local.md # Runtime state / session lock (gitignored)
|
|
366
|
+
│
|
|
367
|
+
│── # ─── Gemini CLI (when selected) ────
|
|
368
|
+
├── .gemini/
|
|
369
|
+
│ ├── commands/
|
|
370
|
+
│ │ └── chati.toml # TOML command → orchestrator
|
|
371
|
+
│ ├── context/ # 4 framework context files (@imported by GEMINI.md)
|
|
372
|
+
│ ├── hooks/ # 6 hooks (BeforeModel, BeforeTool, PreCompress)
|
|
373
|
+
│ ├── settings.json # Hook configuration
|
|
374
|
+
│ └── session-lock.md # Runtime state / session lock (gitignored)
|
|
375
|
+
├── GEMINI.md # Project context with @import chain
|
|
376
|
+
│
|
|
377
|
+
│── # ─── Codex CLI (when selected) ─────
|
|
378
|
+
├── .agents/skills/chati/
|
|
379
|
+
│ └── SKILL.md # Skill definition → orchestrator
|
|
380
|
+
├── .codex/rules/ # Starlark execution policies (constitution-guard, read-protection)
|
|
381
|
+
├── AGENTS.md # Project context with inline governance
|
|
382
|
+
├── AGENTS.override.md # Runtime state / session lock (gitignored)
|
|
383
|
+
│
|
|
384
|
+
│── # ─── Framework ─────────────────────
|
|
354
385
|
├── chati.dev/
|
|
355
386
|
│ ├── orchestrator/ # Main orchestrator
|
|
356
387
|
│ ├── agents/ # 13 agent definitions
|
|
@@ -364,8 +395,8 @@ your-project/
|
|
|
364
395
|
│ ├── schemas/ # 5 JSON schemas
|
|
365
396
|
│ ├── intelligence/ # PRISM, RECALL, COMPASS specs
|
|
366
397
|
│ ├── domains/ # Per-agent and per-workflow configs
|
|
367
|
-
│ ├── hooks/ # 6 Claude
|
|
368
|
-
│ ├── context/ # Context files (deployed
|
|
398
|
+
│ ├── hooks/ # 6 shared hooks (used by Claude + Gemini)
|
|
399
|
+
│ ├── context/ # Context files (deployed per provider)
|
|
369
400
|
│ ├── frameworks/ # Decision heuristics
|
|
370
401
|
│ ├── quality-gates/ # Planning & implementation gates
|
|
371
402
|
│ ├── patterns/ # Elicitation patterns
|
package/framework/config.yaml
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# chati.dev Configuration
|
|
2
|
-
version: "3.2.
|
|
2
|
+
version: "3.2.2"
|
|
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.2"
|
|
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,38 @@ 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'],
|
|
66
|
+
['/chati', '$chati'],
|
|
64
67
|
];
|
|
65
68
|
|
|
66
69
|
// ---------------------------------------------------------------------------
|
|
67
70
|
// Generators
|
|
68
71
|
// ---------------------------------------------------------------------------
|
|
69
72
|
|
|
73
|
+
/**
|
|
74
|
+
* @import directives appended to GEMINI.md.
|
|
75
|
+
* Gemini CLI resolves @import automatically, loading the full governance
|
|
76
|
+
* context chain + session lock — equivalent to Claude Code's rules/ + CLAUDE.local.md.
|
|
77
|
+
*/
|
|
78
|
+
const GEMINI_IMPORTS = [
|
|
79
|
+
'@import .gemini/context/root.md',
|
|
80
|
+
'@import .gemini/context/governance.md',
|
|
81
|
+
'@import .gemini/context/protocols.md',
|
|
82
|
+
'@import .gemini/context/quality.md',
|
|
83
|
+
'@import .gemini/session-lock.md',
|
|
84
|
+
];
|
|
85
|
+
|
|
70
86
|
/**
|
|
71
87
|
* Generate GEMINI.md content from CLAUDE.md content.
|
|
72
88
|
*
|
|
73
89
|
* Adapts the content by replacing Claude Code-specific references with
|
|
74
|
-
* Gemini CLI equivalents
|
|
90
|
+
* Gemini CLI equivalents, then appends @import directives so Gemini CLI
|
|
91
|
+
* auto-loads governance rules and session lock (context parity with Claude).
|
|
75
92
|
*
|
|
76
93
|
* @param {string} content - Raw CLAUDE.md content
|
|
77
94
|
* @returns {string} Adapted content for GEMINI.md
|
|
@@ -89,7 +106,10 @@ export function generateGeminiMd(content) {
|
|
|
89
106
|
'',
|
|
90
107
|
].join('\n');
|
|
91
108
|
|
|
92
|
-
|
|
109
|
+
// Append @import directives for context parity
|
|
110
|
+
const imports = '\n' + GEMINI_IMPORTS.join('\n') + '\n';
|
|
111
|
+
|
|
112
|
+
return header + result + imports;
|
|
93
113
|
}
|
|
94
114
|
|
|
95
115
|
/**
|
|
@@ -97,12 +117,14 @@ export function generateGeminiMd(content) {
|
|
|
97
117
|
*
|
|
98
118
|
* Simplifies the content for Codex CLI: strips hook references,
|
|
99
119
|
* replaces CLI-specific paths, and produces a leaner context file
|
|
100
|
-
* focused on code execution.
|
|
120
|
+
* focused on code execution. When contextFiles are provided, inlines
|
|
121
|
+
* governance/protocols/quality for full context parity with Claude Code.
|
|
101
122
|
*
|
|
102
123
|
* @param {string} content - Raw CLAUDE.md content
|
|
124
|
+
* @param {object} [contextFiles] - Optional inline context { root, governance, protocols, quality }
|
|
103
125
|
* @returns {string} Adapted content for AGENTS.md
|
|
104
126
|
*/
|
|
105
|
-
export function generateAgentsMd(content) {
|
|
127
|
+
export function generateAgentsMd(content, contextFiles = null) {
|
|
106
128
|
let result = content;
|
|
107
129
|
|
|
108
130
|
// Strip hook-related sections (Codex has no hooks support)
|
|
@@ -124,13 +146,39 @@ export function generateAgentsMd(content) {
|
|
|
124
146
|
'',
|
|
125
147
|
].join('\n');
|
|
126
148
|
|
|
127
|
-
|
|
149
|
+
result = header + result;
|
|
150
|
+
|
|
151
|
+
// Inline context files for full governance parity (Codex has no @import)
|
|
152
|
+
if (contextFiles) {
|
|
153
|
+
const sections = [];
|
|
154
|
+
for (const [name, fileContent] of Object.entries(contextFiles)) {
|
|
155
|
+
if (fileContent) {
|
|
156
|
+
// Adapt context for Codex (replace Claude-specific refs)
|
|
157
|
+
let adapted = fileContent;
|
|
158
|
+
for (const [search, replace] of CODEX_REPLACEMENTS) {
|
|
159
|
+
adapted = adapted.replaceAll(search, replace);
|
|
160
|
+
}
|
|
161
|
+
sections.push(adapted.trim());
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
if (sections.length > 0) {
|
|
165
|
+
result += '\n---\n\n' + sections.join('\n\n---\n\n') + '\n';
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return result;
|
|
128
170
|
}
|
|
129
171
|
|
|
130
172
|
// ---------------------------------------------------------------------------
|
|
131
173
|
// Orchestrator
|
|
132
174
|
// ---------------------------------------------------------------------------
|
|
133
175
|
|
|
176
|
+
/**
|
|
177
|
+
* Context file names that provide governance parity across CLIs.
|
|
178
|
+
* These are read from chati.dev/context/ and inlined into AGENTS.md for Codex.
|
|
179
|
+
*/
|
|
180
|
+
const CONTEXT_FILES = ['root.md', 'governance.md', 'protocols.md', 'quality.md'];
|
|
181
|
+
|
|
134
182
|
/**
|
|
135
183
|
* Generate context files for all enabled alternative providers.
|
|
136
184
|
*
|
|
@@ -138,6 +186,8 @@ export function generateAgentsMd(content) {
|
|
|
138
186
|
* CLAUDE.md from the project root, and writes the appropriate context
|
|
139
187
|
* files (GEMINI.md, AGENTS.md) when their providers are active.
|
|
140
188
|
*
|
|
189
|
+
* For Codex, governance context files are inlined into AGENTS.md to provide
|
|
190
|
+
* the same rules awareness that Claude gets from .claude/rules/chati/.
|
|
141
191
|
*
|
|
142
192
|
* @param {string} projectDir - Project root directory
|
|
143
193
|
* @param {string} [baseContent] - Optional base content (used when CLAUDE.md doesn't exist on disk)
|
|
@@ -160,15 +210,18 @@ export function generateContextFiles(projectDir, baseContent = null) {
|
|
|
160
210
|
// Determine enabled providers from config.yaml
|
|
161
211
|
const enabledProviders = resolveEnabledProviders(projectDir);
|
|
162
212
|
|
|
213
|
+
// Read context files for inline injection (Codex)
|
|
214
|
+
const contextFiles = readContextFilesFromDisk(projectDir);
|
|
215
|
+
|
|
163
216
|
// Provider-to-generator mapping
|
|
164
217
|
const generators = {
|
|
165
218
|
gemini: {
|
|
166
219
|
filename: 'GEMINI.md',
|
|
167
|
-
generate: generateGeminiMd,
|
|
220
|
+
generate: (content) => generateGeminiMd(content),
|
|
168
221
|
},
|
|
169
222
|
codex: {
|
|
170
223
|
filename: 'AGENTS.md',
|
|
171
|
-
generate: generateAgentsMd,
|
|
224
|
+
generate: (content) => generateAgentsMd(content, contextFiles),
|
|
172
225
|
},
|
|
173
226
|
};
|
|
174
227
|
|
|
@@ -186,6 +239,30 @@ export function generateContextFiles(projectDir, baseContent = null) {
|
|
|
186
239
|
return result;
|
|
187
240
|
}
|
|
188
241
|
|
|
242
|
+
/**
|
|
243
|
+
* Read context files from chati.dev/context/ for inline injection.
|
|
244
|
+
* Returns null if no context files found (backward compat).
|
|
245
|
+
*
|
|
246
|
+
* @param {string} projectDir - Project root directory
|
|
247
|
+
* @returns {object|null} { root, governance, protocols, quality } or null
|
|
248
|
+
*/
|
|
249
|
+
function readContextFilesFromDisk(projectDir) {
|
|
250
|
+
const contextDir = join(projectDir, 'chati.dev', 'context');
|
|
251
|
+
if (!existsSync(contextDir)) return null;
|
|
252
|
+
|
|
253
|
+
const files = {};
|
|
254
|
+
let found = false;
|
|
255
|
+
for (const file of CONTEXT_FILES) {
|
|
256
|
+
const filePath = join(contextDir, file);
|
|
257
|
+
if (existsSync(filePath)) {
|
|
258
|
+
const key = file.replace('.md', '');
|
|
259
|
+
files[key] = readFileSync(filePath, 'utf-8');
|
|
260
|
+
found = true;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return found ? files : null;
|
|
264
|
+
}
|
|
265
|
+
|
|
189
266
|
// ---------------------------------------------------------------------------
|
|
190
267
|
// Helpers
|
|
191
268
|
// ---------------------------------------------------------------------------
|
|
@@ -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
|
|
|
@@ -103,6 +103,11 @@ function buildReplacementConfig(provider) {
|
|
|
103
103
|
['/model haiku', `/model ${minimal}`],
|
|
104
104
|
];
|
|
105
105
|
|
|
106
|
+
// Codex CLI uses $chati for skill invocation, not /chati
|
|
107
|
+
if (provider === 'codex') {
|
|
108
|
+
strings.push(['/chati', '$chati']);
|
|
109
|
+
}
|
|
110
|
+
|
|
106
111
|
// --- Regex patterns for model names in structured contexts ---
|
|
107
112
|
const patterns = [
|
|
108
113
|
// Identity section: **Model**: opus | ...
|
|
@@ -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
|
+
}
|