chati-dev 4.2.0 → 4.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/framework/config.yaml +3 -3
- package/framework/constitution.md +3 -1
- package/framework/context/governance.md +24 -1
- package/framework/context/quality.md +14 -1
- package/framework/context/root.md +4 -3
- package/framework/hooks/constitution-guard.js +69 -33
- package/framework/hooks/license-guard.js +92 -188
- package/framework/hooks/mode-governance.js +55 -14
- package/framework/hooks/model-governance.js +18 -8
- package/framework/hooks/package.json +3 -0
- package/framework/hooks/prism-engine.js +22 -8
- package/framework/hooks/read-protection.js +37 -9
- package/framework/hooks/session-digest.js +45 -20
- package/framework/hooks/style-guard.js +30 -10
- package/framework/hooks/team-quality-gate.js +39 -13
- package/framework/hooks/undercover-guard.js +30 -11
- package/framework/orchestrator/chati.md +31 -28
- package/package.json +1 -1
- package/scripts/validate-package.js +256 -8
- package/src/config/claude-settings-generator.js +206 -0
- package/src/config/gemini-hooks-generator.js +58 -0
- package/src/installer/core.js +223 -129
- package/src/installer/templates.js +38 -0
- package/src/orchestrator/cli.js +41 -11
|
@@ -22,6 +22,10 @@
|
|
|
22
22
|
* Maps each chati.dev hook to its Gemini CLI equivalent event.
|
|
23
23
|
*/
|
|
24
24
|
export const HOOK_MAP = {
|
|
25
|
+
// license-guard MUST be first on BeforeModel so license check runs before
|
|
26
|
+
// any model call. Without this, an expired trial could keep using Gemini
|
|
27
|
+
// indefinitely from a long-running session.
|
|
28
|
+
'license-guard': { event: 'BeforeModel', description: 'Validate license on every turn (chati.dev governance)' },
|
|
25
29
|
'prism-engine': { event: 'BeforeModel', description: 'Inject PRISM context into model prompt' },
|
|
26
30
|
'model-governance': { event: 'BeforeModel', description: 'Advisory: recommended model per agent' },
|
|
27
31
|
'mode-governance': { event: 'BeforeTool', description: 'Block writes outside current mode scope' },
|
|
@@ -611,6 +615,7 @@ main();
|
|
|
611
615
|
*/
|
|
612
616
|
export function generateAllGeminiHooks() {
|
|
613
617
|
return {
|
|
618
|
+
'license-guard.js': generateLicenseGuard(),
|
|
614
619
|
'prism-engine.js': generatePrismEngine(),
|
|
615
620
|
'model-governance.js': generateModelGovernance(),
|
|
616
621
|
'mode-governance.js': generateModeGovernance(),
|
|
@@ -622,6 +627,59 @@ export function generateAllGeminiHooks() {
|
|
|
622
627
|
};
|
|
623
628
|
}
|
|
624
629
|
|
|
630
|
+
/**
|
|
631
|
+
* Generate the license guard hook for Gemini CLI.
|
|
632
|
+
* BeforeModel event — validates license on every turn.
|
|
633
|
+
*
|
|
634
|
+
* Delegates to chati.dev/hooks/license-guard.js (canonical implementation)
|
|
635
|
+
* via dynamic import of checkLicense(). Translates the verdict object to
|
|
636
|
+
* Gemini's blocking schema (exit code 2 + stderr is the universal block).
|
|
637
|
+
*/
|
|
638
|
+
function generateLicenseGuard() {
|
|
639
|
+
return `${HOOK_HEADER}
|
|
640
|
+
async function main() {
|
|
641
|
+
let input = '';
|
|
642
|
+
for await (const chunk of process.stdin) {
|
|
643
|
+
input += chunk;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
try {
|
|
647
|
+
const event = JSON.parse(input);
|
|
648
|
+
const cwd = event.cwd || process.cwd();
|
|
649
|
+
|
|
650
|
+
// Delegate to canonical license check
|
|
651
|
+
const hookPath = join(cwd, 'chati.dev', 'hooks', 'license-guard.js');
|
|
652
|
+
if (!existsSync(hookPath)) {
|
|
653
|
+
// No canonical hook present — fail open
|
|
654
|
+
console.log(JSON.stringify({}));
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
const mod = await import(hookPath);
|
|
659
|
+
if (typeof mod.checkLicense !== 'function') {
|
|
660
|
+
console.log(JSON.stringify({}));
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
const result = await mod.checkLicense();
|
|
665
|
+
if (result.valid) {
|
|
666
|
+
console.log(JSON.stringify({}));
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// Block via exit code 2 (universal Gemini block) + stderr message.
|
|
671
|
+
process.stderr.write(result.reason || 'License invalid');
|
|
672
|
+
process.exit(2);
|
|
673
|
+
} catch {
|
|
674
|
+
// Fail open on any parse/import error — never block on bugs.
|
|
675
|
+
console.log(JSON.stringify({}));
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
main();
|
|
680
|
+
`;
|
|
681
|
+
}
|
|
682
|
+
|
|
625
683
|
/**
|
|
626
684
|
* Generate .gemini/settings.json content with hook configuration.
|
|
627
685
|
*
|
package/src/installer/core.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mkdirSync, writeFileSync, copyFileSync, existsSync, readFileSync } from 'fs';
|
|
1
|
+
import { mkdirSync, writeFileSync, copyFileSync, existsSync, readFileSync, readdirSync } from 'fs';
|
|
2
2
|
import { join, dirname } from 'path';
|
|
3
3
|
import { fileURLToPath } from 'url';
|
|
4
4
|
import { IDE_CONFIGS, IDE_TO_PROVIDER } from '../config/ide-configs.js';
|
|
@@ -6,6 +6,7 @@ import { generateClaudeMCPConfig } from '../config/mcp-configs.js';
|
|
|
6
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
|
+
import { generateClaudeSettings } from '../config/claude-settings-generator.js';
|
|
9
10
|
import { generateProviderOverlays } from './provider-overlay.js';
|
|
10
11
|
import { verifyManifest } from './manifest.js';
|
|
11
12
|
|
|
@@ -146,144 +147,103 @@ export async function installFramework(config) {
|
|
|
146
147
|
}
|
|
147
148
|
|
|
148
149
|
/**
|
|
149
|
-
*
|
|
150
|
+
* Framework directories to copy recursively from source to target.
|
|
151
|
+
* This list controls WHICH top-level directories under chati.dev/ get copied.
|
|
152
|
+
* Everything inside each directory is copied automatically — no need to
|
|
153
|
+
* enumerate individual files.
|
|
154
|
+
*
|
|
155
|
+
* New files added to the source framework are picked up automatically.
|
|
150
156
|
*/
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
'agents/plan/phases.md',
|
|
171
|
-
'agents/plan/tasks.md',
|
|
172
|
-
// Quality agents
|
|
173
|
-
'agents/quality/qa-planning.md',
|
|
174
|
-
'agents/quality/qa-implementation.md',
|
|
175
|
-
// BUILD + DEPLOY agents
|
|
176
|
-
'agents/build/dev.md',
|
|
177
|
-
'agents/deploy/devops.md',
|
|
178
|
-
// Templates
|
|
179
|
-
'templates/prd-tmpl.yaml',
|
|
180
|
-
'templates/brownfield-prd-tmpl.yaml',
|
|
181
|
-
'templates/fullstack-architecture-tmpl.yaml',
|
|
182
|
-
'templates/task-tmpl.yaml',
|
|
183
|
-
'templates/qa-gate-tmpl.yaml',
|
|
184
|
-
'templates/quick-brief-tmpl.yaml',
|
|
185
|
-
'templates/brandbook-tmpl.yaml',
|
|
186
|
-
'templates/brandbook-html-tmpl.md',
|
|
187
|
-
'templates/design-token-tmpl.yaml',
|
|
188
|
-
'templates/component-spec-tmpl.yaml',
|
|
189
|
-
'templates/icon-system-tmpl.yaml',
|
|
190
|
-
'templates/session-memory-tmpl.yaml',
|
|
191
|
-
// Workflows
|
|
192
|
-
'workflows/greenfield-fullstack.yaml',
|
|
193
|
-
'workflows/brownfield-fullstack.yaml',
|
|
194
|
-
'workflows/brownfield-discovery.yaml',
|
|
195
|
-
'workflows/brownfield-service.yaml',
|
|
196
|
-
'workflows/brownfield-ui.yaml',
|
|
197
|
-
'workflows/quick-flow.yaml',
|
|
198
|
-
'workflows/standard-flow.yaml',
|
|
199
|
-
// Quality gates
|
|
200
|
-
'quality-gates/planning-gate.md',
|
|
201
|
-
'quality-gates/implementation-gate.md',
|
|
202
|
-
// Schemas
|
|
203
|
-
'schemas/session.schema.json',
|
|
204
|
-
'schemas/config.schema.json',
|
|
205
|
-
'schemas/task.schema.json',
|
|
206
|
-
'schemas/context.schema.json',
|
|
207
|
-
'schemas/memory.schema.json',
|
|
208
|
-
// Frameworks
|
|
209
|
-
'frameworks/quality-dimensions.yaml',
|
|
210
|
-
'frameworks/decision-heuristics.yaml',
|
|
211
|
-
// Intelligence
|
|
212
|
-
'intelligence/gotchas.yaml',
|
|
213
|
-
'intelligence/patterns.yaml',
|
|
214
|
-
'intelligence/confidence.yaml',
|
|
215
|
-
'intelligence/context-engine.md',
|
|
216
|
-
'intelligence/memory-layer.md',
|
|
217
|
-
'intelligence/decision-engine.md',
|
|
218
|
-
// Patterns
|
|
219
|
-
'patterns/elicitation.md',
|
|
220
|
-
'patterns/elicitation-library.yaml',
|
|
221
|
-
// Hooks
|
|
222
|
-
'hooks/prism-engine.js',
|
|
223
|
-
'hooks/mode-governance.js',
|
|
224
|
-
'hooks/constitution-guard.js',
|
|
225
|
-
'hooks/session-digest.js',
|
|
226
|
-
'hooks/model-governance.js',
|
|
227
|
-
'hooks/settings.json',
|
|
228
|
-
'hooks/read-protection.js',
|
|
229
|
-
// Domains (PRISM Context Engine)
|
|
230
|
-
'domains/constitution.yaml',
|
|
231
|
-
'domains/global.yaml',
|
|
232
|
-
'domains/agents/orchestrator.yaml',
|
|
233
|
-
'domains/agents/greenfield-wu.yaml',
|
|
234
|
-
'domains/agents/brownfield-wu.yaml',
|
|
235
|
-
'domains/agents/brief.yaml',
|
|
236
|
-
'domains/agents/detail.yaml',
|
|
237
|
-
'domains/agents/architect.yaml',
|
|
238
|
-
'domains/agents/ux.yaml',
|
|
239
|
-
'domains/agents/phases.yaml',
|
|
240
|
-
'domains/agents/tasks.yaml',
|
|
241
|
-
'domains/agents/qa-planning.yaml',
|
|
242
|
-
'domains/agents/qa-implementation.yaml',
|
|
243
|
-
'domains/agents/dev.yaml',
|
|
244
|
-
'domains/agents/devops.yaml',
|
|
245
|
-
'domains/workflows/greenfield-fullstack.yaml',
|
|
246
|
-
'domains/workflows/brownfield-fullstack.yaml',
|
|
247
|
-
'domains/workflows/brownfield-discovery.yaml',
|
|
248
|
-
'domains/workflows/brownfield-service.yaml',
|
|
249
|
-
'domains/workflows/brownfield-ui.yaml',
|
|
250
|
-
'domains/workflows/quick-flow.yaml',
|
|
251
|
-
'domains/workflows/standard-flow.yaml',
|
|
252
|
-
// i18n
|
|
253
|
-
'i18n/en.yaml',
|
|
254
|
-
'i18n/pt.yaml',
|
|
255
|
-
'i18n/es.yaml',
|
|
256
|
-
'i18n/fr.yaml',
|
|
257
|
-
// Migrations
|
|
258
|
-
'migrations/v1.0-to-v1.1.yaml',
|
|
259
|
-
// Data
|
|
260
|
-
'data/entity-registry.yaml',
|
|
261
|
-
// Context (@ import chain for CLAUDE.md)
|
|
262
|
-
'context/root.md',
|
|
263
|
-
'context/governance.md',
|
|
264
|
-
'context/protocols.md',
|
|
265
|
-
'context/quality.md',
|
|
266
|
-
];
|
|
267
|
-
|
|
268
|
-
for (const file of filesToCopy) {
|
|
269
|
-
const src = join(FRAMEWORK_SOURCE, file);
|
|
270
|
-
const dest = join(destDir, file);
|
|
157
|
+
const FRAMEWORK_DIRS_TO_COPY = [
|
|
158
|
+
'orchestrator',
|
|
159
|
+
'agents',
|
|
160
|
+
'templates',
|
|
161
|
+
'workflows',
|
|
162
|
+
'quality-gates',
|
|
163
|
+
'schemas',
|
|
164
|
+
'frameworks',
|
|
165
|
+
'intelligence',
|
|
166
|
+
'patterns',
|
|
167
|
+
'hooks',
|
|
168
|
+
'domains',
|
|
169
|
+
'i18n',
|
|
170
|
+
'migrations',
|
|
171
|
+
'data',
|
|
172
|
+
'context',
|
|
173
|
+
'tasks',
|
|
174
|
+
'presets',
|
|
175
|
+
];
|
|
271
176
|
|
|
272
|
-
|
|
273
|
-
|
|
177
|
+
/**
|
|
178
|
+
* Loose files at the root of chati.dev/ that get copied directly.
|
|
179
|
+
*/
|
|
180
|
+
const FRAMEWORK_ROOT_FILES = [
|
|
181
|
+
'constitution.md',
|
|
182
|
+
];
|
|
274
183
|
|
|
275
|
-
|
|
184
|
+
/**
|
|
185
|
+
* Recursively copy a directory from source to destination, applying provider
|
|
186
|
+
* adaptation for files listed in ADAPTABLE_FILES when the provider is not Claude.
|
|
187
|
+
*/
|
|
188
|
+
function copyDirRecursive(srcDir, destDir, provider, relBase = '') {
|
|
189
|
+
if (!existsSync(srcDir)) return;
|
|
190
|
+
createDir(destDir);
|
|
191
|
+
|
|
192
|
+
const entries = readdirSync(srcDir, { withFileTypes: true });
|
|
193
|
+
for (const entry of entries) {
|
|
194
|
+
const srcPath = join(srcDir, entry.name);
|
|
195
|
+
const destPath = join(destDir, entry.name);
|
|
196
|
+
const relPath = relBase ? join(relBase, entry.name) : entry.name;
|
|
197
|
+
|
|
198
|
+
if (entry.isDirectory()) {
|
|
199
|
+
copyDirRecursive(srcPath, destPath, provider, relPath);
|
|
200
|
+
} else if (entry.isFile()) {
|
|
201
|
+
if (provider !== 'claude' && ADAPTABLE_FILES.has(relPath)) {
|
|
276
202
|
// Non-Claude provider: read, adapt, write
|
|
277
|
-
const content = readFileSync(
|
|
278
|
-
writeFileSync(
|
|
203
|
+
const content = readFileSync(srcPath, 'utf-8');
|
|
204
|
+
writeFileSync(destPath, adaptFrameworkFile(content, relPath, provider), 'utf-8');
|
|
279
205
|
} else {
|
|
280
206
|
// Claude or non-adaptable: direct copy
|
|
281
|
-
copyFileSync(
|
|
207
|
+
copyFileSync(srcPath, destPath);
|
|
282
208
|
}
|
|
283
209
|
}
|
|
284
210
|
}
|
|
285
211
|
}
|
|
286
212
|
|
|
213
|
+
/**
|
|
214
|
+
* Copy framework files from the Chati.dev source directory.
|
|
215
|
+
*
|
|
216
|
+
* Uses recursive directory copy instead of a hardcoded file list, so that
|
|
217
|
+
* new files added to the framework source are automatically picked up.
|
|
218
|
+
*
|
|
219
|
+
* @param {string} destDir - Target framework directory (e.g., {project}/chati.dev/)
|
|
220
|
+
* @param {string} provider - Primary provider for text adaptation ('claude' | 'gemini' | 'codex')
|
|
221
|
+
*/
|
|
222
|
+
export function copyFrameworkFiles(destDir, provider = 'claude') {
|
|
223
|
+
if (!existsSync(FRAMEWORK_SOURCE)) return;
|
|
224
|
+
|
|
225
|
+
// Copy loose root files
|
|
226
|
+
for (const file of FRAMEWORK_ROOT_FILES) {
|
|
227
|
+
const src = join(FRAMEWORK_SOURCE, file);
|
|
228
|
+
if (!existsSync(src)) continue;
|
|
229
|
+
const dest = join(destDir, file);
|
|
230
|
+
createDir(dirname(dest));
|
|
231
|
+
if (provider !== 'claude' && ADAPTABLE_FILES.has(file)) {
|
|
232
|
+
const content = readFileSync(src, 'utf-8');
|
|
233
|
+
writeFileSync(dest, adaptFrameworkFile(content, file, provider), 'utf-8');
|
|
234
|
+
} else {
|
|
235
|
+
copyFileSync(src, dest);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Copy each framework directory recursively
|
|
240
|
+
for (const dir of FRAMEWORK_DIRS_TO_COPY) {
|
|
241
|
+
const srcDir = join(FRAMEWORK_SOURCE, dir);
|
|
242
|
+
const destSubDir = join(destDir, dir);
|
|
243
|
+
copyDirRecursive(srcDir, destSubDir, provider, dir);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
287
247
|
/**
|
|
288
248
|
* Configure a specific IDE
|
|
289
249
|
*/
|
|
@@ -331,6 +291,12 @@ Pass through all context: session state, handoffs, artifacts, and user input.
|
|
|
331
291
|
`;
|
|
332
292
|
writeFileSync(join(targetDir, '.claude', 'commands', 'chati.md'), routerContent, 'utf-8');
|
|
333
293
|
|
|
294
|
+
// .claude/settings.json — wires the 10 chati.dev hooks into Claude Code's
|
|
295
|
+
// hook system and applies a baseline permission policy. THIS IS THE FILE
|
|
296
|
+
// CLAUDE CODE READS — without it, every hook in chati.dev/hooks/ is dormant.
|
|
297
|
+
// (Pre-v4.2.2 bug: this file was never written, hooks never ran.)
|
|
298
|
+
writeClaudeSettingsWithMerge(join(targetDir, '.claude', 'settings.json'));
|
|
299
|
+
|
|
334
300
|
// MCP config
|
|
335
301
|
if (selectedMCPs.length > 0) {
|
|
336
302
|
const mcpConfig = generateClaudeMCPConfig(selectedMCPs);
|
|
@@ -348,10 +314,35 @@ Pass through all context: session state, handoffs, artifacts, and user input.
|
|
|
348
314
|
// Session lock override file (equivalent to CLAUDE.local.md)
|
|
349
315
|
writeFileSync(join(targetDir, 'AGENTS.override.md'), generateAgentsOverrideMd(), 'utf-8');
|
|
350
316
|
|
|
351
|
-
// Starlark execution policies (
|
|
317
|
+
// Starlark execution policies (Codex sandboxed rule engine)
|
|
352
318
|
createDir(join(targetDir, '.codex', 'rules'));
|
|
353
319
|
writeFileSync(join(targetDir, '.codex', 'rules', 'constitution-guard.rules'), generateCodexConstitutionGuardRules(), 'utf-8');
|
|
354
320
|
writeFileSync(join(targetDir, '.codex', 'rules', 'read-protection.rules'), generateCodexReadProtectionRules(), 'utf-8');
|
|
321
|
+
|
|
322
|
+
// .codex/hooks.json — license-guard wired to UserPromptSubmit (per-turn enforcement).
|
|
323
|
+
// Codex hooks.json is experimental (requires [features] codex_hooks = true in
|
|
324
|
+
// codex config). Without it, license enforcement only happens at slash command
|
|
325
|
+
// entry — a long-running terminal session would not be re-validated.
|
|
326
|
+
writeFileSync(
|
|
327
|
+
join(targetDir, '.codex', 'hooks.json'),
|
|
328
|
+
JSON.stringify({
|
|
329
|
+
$comment: 'chati.dev v4.2.2 — experimental Codex hooks. Requires [features] codex_hooks = true.',
|
|
330
|
+
hooks: {
|
|
331
|
+
UserPromptSubmit: [
|
|
332
|
+
{
|
|
333
|
+
matcher: '.*',
|
|
334
|
+
hooks: [
|
|
335
|
+
{
|
|
336
|
+
type: 'command',
|
|
337
|
+
command: 'node chati.dev/hooks/license-guard.js',
|
|
338
|
+
},
|
|
339
|
+
],
|
|
340
|
+
},
|
|
341
|
+
],
|
|
342
|
+
},
|
|
343
|
+
}, null, 2) + '\n',
|
|
344
|
+
'utf-8'
|
|
345
|
+
);
|
|
355
346
|
} else if (ideKey === 'gemini-cli') {
|
|
356
347
|
// Gemini CLI: TOML command file (native format for /chati command)
|
|
357
348
|
writeFileSync(join(targetDir, '.gemini', 'commands', 'chati.toml'), generateGeminiRouter({ orchestratorPath }), 'utf-8');
|
|
@@ -471,3 +462,106 @@ function createDir(dir) {
|
|
|
471
462
|
mkdirSync(dir, { recursive: true });
|
|
472
463
|
}
|
|
473
464
|
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Write .claude/settings.json with safe merge against existing user content.
|
|
468
|
+
*
|
|
469
|
+
* If the file does not exist: write fresh chati settings.
|
|
470
|
+
* If the file exists and is valid JSON: merge — chati hooks/permissions are
|
|
471
|
+
* UNIONED into existing values rather than replacing them. User customizations
|
|
472
|
+
* (env, statusLine, plugins, language, alwaysThinkingEnabled, etc.) are preserved.
|
|
473
|
+
* If the file exists but is malformed: leave it alone, log a warning. We will
|
|
474
|
+
* not silently destroy user data.
|
|
475
|
+
*/
|
|
476
|
+
function writeClaudeSettingsWithMerge(settingsPath) {
|
|
477
|
+
createDir(dirname(settingsPath));
|
|
478
|
+
const fresh = JSON.parse(generateClaudeSettings());
|
|
479
|
+
|
|
480
|
+
if (!existsSync(settingsPath)) {
|
|
481
|
+
writeFileSync(settingsPath, JSON.stringify(fresh, null, 2) + '\n', 'utf-8');
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
let existing;
|
|
486
|
+
try {
|
|
487
|
+
existing = JSON.parse(readFileSync(settingsPath, 'utf-8'));
|
|
488
|
+
} catch (err) {
|
|
489
|
+
process.stderr.write(`[chati] WARNING: .claude/settings.json exists but is not valid JSON (${err.message}). Skipping merge — chati hooks will NOT be wired. Fix the file and re-run install.\n`);
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
const merged = mergeClaudeSettings(existing, fresh);
|
|
494
|
+
writeFileSync(settingsPath, JSON.stringify(merged, null, 2) + '\n', 'utf-8');
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Merge two Claude Code settings.json objects.
|
|
499
|
+
*
|
|
500
|
+
* - permissions.allow / permissions.deny: array union (de-duplicated)
|
|
501
|
+
* - permissions.defaultMode: keep existing if set, else use fresh
|
|
502
|
+
* - hooks: per-event union — chati hook commands are appended to existing
|
|
503
|
+
* matcher groups; user's hooks for the same events are preserved
|
|
504
|
+
* - $schema: prefer fresh (always our canonical URL)
|
|
505
|
+
* - all other top-level keys (env, statusLine, plugins, language, etc.):
|
|
506
|
+
* keep existing untouched
|
|
507
|
+
*/
|
|
508
|
+
export function mergeClaudeSettings(existing, fresh) {
|
|
509
|
+
const merged = { ...existing };
|
|
510
|
+
|
|
511
|
+
// $schema — chati's canonical URL wins
|
|
512
|
+
if (fresh.$schema) merged.$schema = fresh.$schema;
|
|
513
|
+
|
|
514
|
+
// permissions
|
|
515
|
+
const existingPerms = existing.permissions || {};
|
|
516
|
+
const freshPerms = fresh.permissions || {};
|
|
517
|
+
merged.permissions = {
|
|
518
|
+
...existingPerms,
|
|
519
|
+
allow: unionArrays(existingPerms.allow, freshPerms.allow),
|
|
520
|
+
deny: unionArrays(existingPerms.deny, freshPerms.deny),
|
|
521
|
+
defaultMode: existingPerms.defaultMode || freshPerms.defaultMode || 'default',
|
|
522
|
+
};
|
|
523
|
+
|
|
524
|
+
// hooks — per-event merge
|
|
525
|
+
const existingHooks = existing.hooks || {};
|
|
526
|
+
const freshHooks = fresh.hooks || {};
|
|
527
|
+
merged.hooks = { ...existingHooks };
|
|
528
|
+
|
|
529
|
+
for (const [eventName, freshGroups] of Object.entries(freshHooks)) {
|
|
530
|
+
const existingGroups = existingHooks[eventName] || [];
|
|
531
|
+
// Append fresh groups; we don't dedup at the group level (different
|
|
532
|
+
// matchers are different groups). Within a single fresh group, the
|
|
533
|
+
// hook commands are unique to chati so duplication risk is minimal.
|
|
534
|
+
// To be safe, dedup by command string across the merged event.
|
|
535
|
+
const seen = new Set();
|
|
536
|
+
const collected = [];
|
|
537
|
+
for (const group of [...existingGroups, ...freshGroups]) {
|
|
538
|
+
const dedupedHooks = (group.hooks || []).filter(h => {
|
|
539
|
+
const key = `${group.matcher || ''}|${h.command}`;
|
|
540
|
+
if (seen.has(key)) return false;
|
|
541
|
+
seen.add(key);
|
|
542
|
+
return true;
|
|
543
|
+
});
|
|
544
|
+
if (dedupedHooks.length > 0) {
|
|
545
|
+
collected.push({
|
|
546
|
+
...(group.matcher !== undefined ? { matcher: group.matcher } : {}),
|
|
547
|
+
hooks: dedupedHooks,
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
merged.hooks[eventName] = collected;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
return merged;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function unionArrays(a, b) {
|
|
558
|
+
const out = [];
|
|
559
|
+
const seen = new Set();
|
|
560
|
+
for (const item of [...(a || []), ...(b || [])]) {
|
|
561
|
+
if (!seen.has(item)) {
|
|
562
|
+
seen.add(item);
|
|
563
|
+
out.push(item);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
return out;
|
|
567
|
+
}
|
|
@@ -108,6 +108,44 @@ export function generateConfigYaml(config) {
|
|
|
108
108
|
},
|
|
109
109
|
};
|
|
110
110
|
|
|
111
|
+
// Feature flags (Intelligence Upgrade + Article XXI Agent Teams)
|
|
112
|
+
// Must match source chati.dev/config.yaml features block.
|
|
113
|
+
configData.features = {
|
|
114
|
+
hybrid_budget: true,
|
|
115
|
+
anti_dash: true,
|
|
116
|
+
rate_limiter_integration: true,
|
|
117
|
+
l5_keywords: true,
|
|
118
|
+
prompt_size_guard: true,
|
|
119
|
+
ids_decision_engine: true,
|
|
120
|
+
surface_criteria: true,
|
|
121
|
+
parallel_fallback: true,
|
|
122
|
+
tool_mesh: true,
|
|
123
|
+
tech_presets: true,
|
|
124
|
+
doctor_autofix: true,
|
|
125
|
+
brandbook: true,
|
|
126
|
+
// Intelligence Upgrade (v4.0.5 — Claude Code patterns)
|
|
127
|
+
undercover_mode: true,
|
|
128
|
+
memory_extraction: true,
|
|
129
|
+
memory_consolidation: true,
|
|
130
|
+
daily_digest: true,
|
|
131
|
+
structured_session_memory: true,
|
|
132
|
+
static_prism_boundary: true,
|
|
133
|
+
token_bracket_estimation: true,
|
|
134
|
+
model_fallback: true,
|
|
135
|
+
frustration_detection: true,
|
|
136
|
+
bash_security_checks: true,
|
|
137
|
+
// Agent Teams (Article XXI) — default ON in v4.2.2.
|
|
138
|
+
// Only effective when provider === 'claude' (gated in cli.js
|
|
139
|
+
// isAgentTeamsEnabled). Gemini and Codex always fall back to sequential
|
|
140
|
+
// pipeline silently. See plan: was previously false for "safe rollout"
|
|
141
|
+
// but the v4.2.0 launch never actually shipped, so this is a fresh start.
|
|
142
|
+
agent_teams: true,
|
|
143
|
+
team_planning_size: 3,
|
|
144
|
+
team_build_size: 2,
|
|
145
|
+
team_echo_threshold: 0.92,
|
|
146
|
+
team_correction_cycles_max: 2,
|
|
147
|
+
};
|
|
148
|
+
|
|
111
149
|
// Add telemetry config
|
|
112
150
|
configData.telemetry = {
|
|
113
151
|
enabled: config.telemetryEnabled !== false,
|
package/src/orchestrator/cli.js
CHANGED
|
@@ -381,24 +381,33 @@ async function handleNext(projectDir) {
|
|
|
381
381
|
// Check for parallel group — with Agent Teams override (Article XXI)
|
|
382
382
|
let action, spawnCommand = null, parallelSpawnCommand = null, parallelAgents = [];
|
|
383
383
|
let teamData = null;
|
|
384
|
+
const teamsEnabled = isAgentTeamsEnabled(projectDir);
|
|
385
|
+
|
|
384
386
|
if (nextInfo.isParallel && nextInfo.group.length > 1) {
|
|
385
|
-
//
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
const teamConfig = TEAM_CONFIGS
|
|
387
|
+
// Planning Team auto-spawn: detail + architect + ux are marked parallel: true
|
|
388
|
+
// in agent-selector. When agent_teams enabled, swap spawn_parallel for spawn_team.
|
|
389
|
+
if (teamsEnabled) {
|
|
390
|
+
const teamConfig = TEAM_CONFIGS.planning;
|
|
389
391
|
const teamId = generateTeamId(teamConfig.slug);
|
|
390
392
|
action = 'spawn_team';
|
|
391
393
|
parallelAgents = teamConfig.members;
|
|
392
|
-
teamData = {
|
|
393
|
-
team_id: teamId,
|
|
394
|
-
team_type: teamType,
|
|
395
|
-
members: teamConfig.members,
|
|
396
|
-
};
|
|
394
|
+
teamData = { team_id: teamId, team_type: 'planning', members: teamConfig.members };
|
|
397
395
|
} else {
|
|
398
396
|
action = 'spawn_parallel';
|
|
399
397
|
parallelAgents = nextInfo.group;
|
|
400
398
|
parallelSpawnCommand = buildParallelSpawnCommand(nextInfo.group, projectDir, lastAgent, modelInfo.provider, 900000);
|
|
401
399
|
}
|
|
400
|
+
} else if (nextAgent === 'dev' && teamsEnabled) {
|
|
401
|
+
// Build Team auto-spawn: dev + qa-implementation are NOT parallel in the
|
|
402
|
+
// classic sense (dev writes, qa reviews per-task), so they're not in a
|
|
403
|
+
// parallel group. But under Agent Teams, they coordinate via mailbox + shared
|
|
404
|
+
// task list, which IS the team mechanism. Spawn Build Team here when dev
|
|
405
|
+
// would otherwise spawn solo.
|
|
406
|
+
const teamConfig = TEAM_CONFIGS.build;
|
|
407
|
+
const teamId = generateTeamId(teamConfig.slug);
|
|
408
|
+
action = 'spawn_team';
|
|
409
|
+
parallelAgents = teamConfig.members;
|
|
410
|
+
teamData = { team_id: teamId, team_type: 'build', members: teamConfig.members };
|
|
402
411
|
} else if (isInteractive) {
|
|
403
412
|
action = 'activate_interactive';
|
|
404
413
|
} else {
|
|
@@ -978,12 +987,33 @@ function generateTeamId(slug) {
|
|
|
978
987
|
return `TM-${date}-${slug}`;
|
|
979
988
|
}
|
|
980
989
|
|
|
990
|
+
/**
|
|
991
|
+
* Agent Teams require BOTH the feature flag AND a Claude provider.
|
|
992
|
+
*
|
|
993
|
+
* Why provider gate: Agent Teams uses Claude Code's native Task tool / subagent
|
|
994
|
+
* spawn API. Gemini and Codex have no equivalent — calling spawn_team for those
|
|
995
|
+
* providers would fail. We always fall back silently to spawn_parallel
|
|
996
|
+
* (Gemini) or spawn_autonomous (Codex) when the active provider is not claude.
|
|
997
|
+
*/
|
|
981
998
|
function isAgentTeamsEnabled(projectDir) {
|
|
982
999
|
const configPath = join(projectDir, 'chati.dev', 'config.yaml');
|
|
983
1000
|
if (!existsSync(configPath)) return false;
|
|
984
1001
|
const raw = readFileSync(configPath, 'utf-8');
|
|
985
|
-
|
|
986
|
-
|
|
1002
|
+
|
|
1003
|
+
// Feature flag check
|
|
1004
|
+
const flagMatch = raw.match(/agent_teams:\s*(true|false)/);
|
|
1005
|
+
const flagOn = flagMatch ? flagMatch[1] === 'true' : false;
|
|
1006
|
+
if (!flagOn) return false;
|
|
1007
|
+
|
|
1008
|
+
// Provider gate — Agent Teams is Claude-only.
|
|
1009
|
+
// Read primary_provider from session.yaml (preferred) or active_provider
|
|
1010
|
+
// field. Default to 'claude' if neither exists (early bootstrap).
|
|
1011
|
+
const sessionPath = join(projectDir, '.chati', 'session.yaml');
|
|
1012
|
+
if (!existsSync(sessionPath)) return true; // No session yet — assume claude
|
|
1013
|
+
const sessionRaw = readFileSync(sessionPath, 'utf-8');
|
|
1014
|
+
const providerMatch = sessionRaw.match(/^\s*(?:active_provider|primary_provider):\s*["']?(\w+)/m);
|
|
1015
|
+
const provider = providerMatch ? providerMatch[1] : 'claude';
|
|
1016
|
+
return provider === 'claude';
|
|
987
1017
|
}
|
|
988
1018
|
|
|
989
1019
|
async function handleSpawnTeam(projectDir, args) {
|