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
|
@@ -83,13 +83,20 @@ Parse the JSON output. The `action` field tells you what to do:
|
|
|
83
83
|
| `complete` | Action: Complete |
|
|
84
84
|
| `error` | Display error, suggest `/chati status` |
|
|
85
85
|
|
|
86
|
-
**
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
86
|
+
**Internal: track context bracket from JSON `context_bracket` but DO NOT display it to the user.** The bracket is internal telemetry — users should never see "Context: FRESH (90%)" or framework jargon like "Initiating handoff protocol". Speak in natural language about what you are doing, not the internal state.
|
|
87
|
+
|
|
88
|
+
Only mention the bracket when it reaches CRITICAL, and only in plain language. Example: "I'm running low on memory — let me wrap up the current step before continuing." Never say "CRITICAL bracket", "PRISM", "L0-L5", "deviation protocol", or other internal framework terms.
|
|
89
|
+
|
|
90
|
+
**Never reveal agent technical names** to the user. Refer to phases naturally:
|
|
91
|
+
- `greenfield-wu` / `brownfield-wu` → "let's understand your goals"
|
|
92
|
+
- `brief` → "let's confirm what we're building"
|
|
93
|
+
- `detail` → "let's expand the requirements"
|
|
94
|
+
- `architect` → "let's design the architecture"
|
|
95
|
+
- `ux` → "let's design the user experience"
|
|
96
|
+
- `phases` / `tasks` → "let's break this down into phases"
|
|
97
|
+
- `qa-planning` / `qa-implementation` → "let's review what we have"
|
|
98
|
+
- `dev` → "let's build it"
|
|
99
|
+
- `devops` → "let's deploy it"
|
|
93
100
|
|
|
94
101
|
---
|
|
95
102
|
|
|
@@ -135,11 +142,7 @@ The user should experience a smooth start: they describe their project, the orch
|
|
|
135
142
|
|
|
136
143
|
These agents (greenfield-wu, brownfield-wu, brief) run in the same conversation.
|
|
137
144
|
|
|
138
|
-
1.
|
|
139
|
-
```
|
|
140
|
-
Model recommendation for {agent}: {model} ({upgrade condition})
|
|
141
|
-
To switch: /model {model}
|
|
142
|
-
```
|
|
145
|
+
1. Track model recommendation from JSON `model_info` internally. Do NOT display "Model recommendation for {agent}" or expose internal agent names. If the recommended model is materially different from the current one and the difference will affect quality, mention it briefly in plain language: e.g., "This step works best on Opus — switch with /model opus if you want stronger reasoning." Otherwise stay silent.
|
|
143
146
|
2. Read the agent file from `agent_file` in the JSON response
|
|
144
147
|
3. Load its full content and **become** that agent
|
|
145
148
|
4. Follow the agent's instructions — the user interacts with you directly
|
|
@@ -206,17 +209,17 @@ Quality gate passed: {score}%.
|
|
|
206
209
|
|
|
207
210
|
These agents run in separate Claude Code processes.
|
|
208
211
|
|
|
209
|
-
1.
|
|
212
|
+
1. Track context bracket internally — do NOT display it.
|
|
210
213
|
2. Check `handoff_status.valid` from JSON:
|
|
211
|
-
- If `false` with missing fields:
|
|
214
|
+
- If `false` with missing fields, present user-facing options in plain language:
|
|
212
215
|
```
|
|
213
|
-
|
|
214
|
-
1.
|
|
215
|
-
2. Continue anyway
|
|
216
|
-
3.
|
|
216
|
+
I noticed some context from the previous step is missing. How should I proceed?
|
|
217
|
+
1. Redo the previous step to regenerate the missing information (Recommended)
|
|
218
|
+
2. Continue anyway, with the missing context
|
|
219
|
+
3. Provide the missing details manually
|
|
217
220
|
```
|
|
218
|
-
- If warnings only:
|
|
219
|
-
- If `true`:
|
|
221
|
+
- If warnings only: proceed silently. Only mention the warning if it directly affects what the user just asked for.
|
|
222
|
+
- If `true`: proceed silently. Do NOT announce "Context check: OK".
|
|
220
223
|
3. Execute `spawn_command` via Bash tool (the full command is in the JSON)
|
|
221
224
|
4. Wait for JSON output from the spawned process. Handle `status`:
|
|
222
225
|
- `"complete"` → Present **Completion Options** (see above) → on approval, run `orchestrate advance --agent {name} --score {score}`
|
|
@@ -240,7 +243,7 @@ Planning phase agents (detail, architect, ux) run simultaneously.
|
|
|
240
243
|
|
|
241
244
|
**NOTE (Article XXI):** When `features.agent_teams: true` in config.yaml, the orchestrator MUST attempt `spawn_team` FIRST for GROUP 1 agents (detail, architect, ux) and BUILD pair (dev, qa-implementation). Only fall back to `spawn_parallel` if team spawning is unavailable or fails. The CLI `orchestrate next` command handles this automatically — when teams are enabled, it returns `spawn_team` instead of `spawn_parallel`.
|
|
242
245
|
|
|
243
|
-
1. Display: "
|
|
246
|
+
1. Display naturally what is happening using user-facing language. Examples (pick one that fits the phase): "Working on the planning phase now." / "Reviewing the architecture and UX in parallel." NEVER say "spawning parallel group", "spawn_parallel", or list internal agent names like "detail, architect, ux".
|
|
244
247
|
2. Execute `parallel_spawn_command` via Bash tool
|
|
245
248
|
3. Parse consolidated JSON output
|
|
246
249
|
4. Present **Completion Options** (Parallel Group variant) — wait for user choice
|
|
@@ -271,7 +274,7 @@ node packages/chati-dev/bin/chati.js orchestrate spawn-team --team-type {plannin
|
|
|
271
274
|
```
|
|
272
275
|
Parse JSON. If `fallback_required: true` → fall back silently to `spawn_parallel`. Do NOT display error messages.
|
|
273
276
|
|
|
274
|
-
Display: "
|
|
277
|
+
Display naturally what the team is doing using user-facing language. For Planning Team: "Working on the planning — designing the requirements, architecture, and user experience together." For Build Team: "Building this with continuous quality review." NEVER say "team", "spawn_team", "Article XXI", "mailbox", "Planning Team", or list internal agent names.
|
|
275
278
|
|
|
276
279
|
### Step 2: Spawn Teammates via Agent Tool
|
|
277
280
|
|
|
@@ -413,13 +416,13 @@ If the Agent tool is unavailable or any sub-agent spawn fails:
|
|
|
413
416
|
|
|
414
417
|
The user is returning to an active session.
|
|
415
418
|
|
|
416
|
-
1. Present
|
|
417
|
-
2.
|
|
418
|
-
3. Offer options:
|
|
419
|
+
1. Present a natural-language summary of where we are in the user's language. Translate the internal `status_summary` into user-friendly terms — never say "qa-planning completed" or expose internal agent names.
|
|
420
|
+
2. Track context bracket internally — do NOT display.
|
|
421
|
+
3. Offer options in plain language (translate internal agent names to user-facing phase descriptions):
|
|
419
422
|
```
|
|
420
|
-
1. Continue
|
|
421
|
-
2. Review
|
|
422
|
-
3.
|
|
423
|
+
1. Continue (Recommended)
|
|
424
|
+
2. Review what we just produced
|
|
425
|
+
3. Show me the full project status
|
|
423
426
|
```
|
|
424
427
|
|
|
425
428
|
---
|
package/package.json
CHANGED
|
@@ -166,6 +166,114 @@ function checkBinExists(packageRoot, results) {
|
|
|
166
166
|
}
|
|
167
167
|
}
|
|
168
168
|
|
|
169
|
+
/**
|
|
170
|
+
* Validate that every hook referenced in hooks/settings.json exists on disk.
|
|
171
|
+
* Prevents shipping a bundle where settings.json references a hook file that
|
|
172
|
+
* was never copied.
|
|
173
|
+
*/
|
|
174
|
+
function checkHooksParity(packageRoot, results) {
|
|
175
|
+
results.checks++;
|
|
176
|
+
const settingsPath = join(packageRoot, 'framework', 'hooks', 'settings.json');
|
|
177
|
+
if (!existsSync(settingsPath)) {
|
|
178
|
+
results.warnings.push('framework/hooks/settings.json not found — skipping hooks parity check.');
|
|
179
|
+
results.passed++;
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
try {
|
|
184
|
+
const settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
|
|
185
|
+
const missing = new Set();
|
|
186
|
+
const hooksDir = join(packageRoot, 'framework', 'hooks');
|
|
187
|
+
|
|
188
|
+
// Walk all hook events and collect referenced .js files
|
|
189
|
+
for (const event of Object.keys(settings.hooks || {})) {
|
|
190
|
+
for (const matcher of settings.hooks[event] || []) {
|
|
191
|
+
for (const hook of matcher.hooks || []) {
|
|
192
|
+
if (hook.type === 'command' && typeof hook.command === 'string') {
|
|
193
|
+
// command format: "node chati.dev/hooks/foo.js"
|
|
194
|
+
const match = hook.command.match(/hooks\/([^\s]+\.js)/);
|
|
195
|
+
if (match) {
|
|
196
|
+
const hookFile = match[1];
|
|
197
|
+
if (!existsSync(join(hooksDir, hookFile))) {
|
|
198
|
+
missing.add(hookFile);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (missing.size === 0) {
|
|
207
|
+
results.passed++;
|
|
208
|
+
} else {
|
|
209
|
+
results.errors.push(`Hooks referenced in settings.json but missing from framework/hooks/: ${[...missing].join(', ')}`);
|
|
210
|
+
}
|
|
211
|
+
} catch (err) {
|
|
212
|
+
results.errors.push(`Failed to parse hooks/settings.json: ${err.message}`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Parity check: every file in source chati.dev/ (that belongs to a copied
|
|
218
|
+
* directory) must exist in framework/. Catches bundling bugs where new files
|
|
219
|
+
* are added to the source but not included in the bundle.
|
|
220
|
+
*
|
|
221
|
+
* Only runs when the monorepo source is available (skipped inside the
|
|
222
|
+
* published package since the source is not shipped).
|
|
223
|
+
*/
|
|
224
|
+
function checkSourceBundleParity(packageRoot, results) {
|
|
225
|
+
results.checks++;
|
|
226
|
+
const monorepoSource = join(packageRoot, '..', '..', 'chati.dev');
|
|
227
|
+
const bundleDir = join(packageRoot, 'framework');
|
|
228
|
+
|
|
229
|
+
if (!existsSync(monorepoSource)) {
|
|
230
|
+
// Running inside the published package — skip (nothing to compare against)
|
|
231
|
+
results.passed++;
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Collect all files in source, grouped by the top-level directory
|
|
236
|
+
const missing = [];
|
|
237
|
+
const COPIED_DIRS = [
|
|
238
|
+
'orchestrator', 'agents', 'templates', 'workflows', 'quality-gates',
|
|
239
|
+
'schemas', 'frameworks', 'intelligence', 'patterns', 'hooks',
|
|
240
|
+
'domains', 'i18n', 'migrations', 'data', 'context', 'tasks', 'presets',
|
|
241
|
+
];
|
|
242
|
+
const COPIED_ROOT_FILES = ['constitution.md'];
|
|
243
|
+
|
|
244
|
+
function walk(srcDir, relBase) {
|
|
245
|
+
if (!existsSync(srcDir)) return;
|
|
246
|
+
for (const entry of readdirSync(srcDir, { withFileTypes: true })) {
|
|
247
|
+
const srcPath = join(srcDir, entry.name);
|
|
248
|
+
const relPath = relBase ? `${relBase}/${entry.name}` : entry.name;
|
|
249
|
+
if (entry.isDirectory()) {
|
|
250
|
+
walk(srcPath, relPath);
|
|
251
|
+
} else if (entry.isFile()) {
|
|
252
|
+
const bundlePath = join(bundleDir, relPath);
|
|
253
|
+
if (!existsSync(bundlePath)) missing.push(relPath);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
for (const file of COPIED_ROOT_FILES) {
|
|
259
|
+
const src = join(monorepoSource, file);
|
|
260
|
+
const bundled = join(bundleDir, file);
|
|
261
|
+
if (existsSync(src) && !existsSync(bundled)) missing.push(file);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
for (const dir of COPIED_DIRS) {
|
|
265
|
+
walk(join(monorepoSource, dir), dir);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (missing.length === 0) {
|
|
269
|
+
results.passed++;
|
|
270
|
+
} else {
|
|
271
|
+
const sample = missing.slice(0, 10).join(', ');
|
|
272
|
+
const more = missing.length > 10 ? ` (+ ${missing.length - 10} more)` : '';
|
|
273
|
+
results.errors.push(`Files in source chati.dev/ missing from framework/ bundle: ${sample}${more}`);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
169
277
|
/**
|
|
170
278
|
* Compare entity count in registry vs actual files in framework/.
|
|
171
279
|
*/
|
|
@@ -201,12 +309,146 @@ function checkEntityCount(packageRoot, results) {
|
|
|
201
309
|
}
|
|
202
310
|
}
|
|
203
311
|
|
|
312
|
+
/**
|
|
313
|
+
* v4.2.2: Validate that generateClaudeSettings() produces a settings.json
|
|
314
|
+
* that wires all 10 chati hooks via the canonical hookSpecificOutput schema
|
|
315
|
+
* — at the right events and with paths that resolve to existing files.
|
|
316
|
+
*
|
|
317
|
+
* This catches the v4.2.0/v4.2.1 catastrophe (hooks dormant because the
|
|
318
|
+
* installer never wrote .claude/settings.json) at validation time.
|
|
319
|
+
*/
|
|
320
|
+
async function checkClaudeHooksWired(packageRoot, results) {
|
|
321
|
+
results.checks++;
|
|
322
|
+
try {
|
|
323
|
+
const genPath = join(packageRoot, 'src', 'config', 'claude-settings-generator.js');
|
|
324
|
+
if (!existsSync(genPath)) {
|
|
325
|
+
results.errors.push('claude-settings-generator.js missing — Claude hooks would not be wired in installs.');
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const mod = await import(genPath);
|
|
330
|
+
if (typeof mod.generateClaudeSettings !== 'function') {
|
|
331
|
+
results.errors.push('claude-settings-generator.js does not export generateClaudeSettings.');
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const settings = JSON.parse(mod.generateClaudeSettings());
|
|
336
|
+
|
|
337
|
+
// Verify required events
|
|
338
|
+
const requiredEvents = ['UserPromptSubmit', 'PreToolUse', 'PreCompact'];
|
|
339
|
+
const missingEvents = requiredEvents.filter(e => !settings.hooks?.[e]);
|
|
340
|
+
if (missingEvents.length > 0) {
|
|
341
|
+
results.errors.push(`generateClaudeSettings missing events: ${missingEvents.join(', ')}`);
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Collect all referenced hook commands
|
|
346
|
+
const allCommands = [];
|
|
347
|
+
for (const event of Object.values(settings.hooks)) {
|
|
348
|
+
for (const group of event) {
|
|
349
|
+
for (const hook of group.hooks || []) {
|
|
350
|
+
allCommands.push(hook.command || '');
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Verify all 10 chati hooks are referenced
|
|
356
|
+
const requiredHooks = [
|
|
357
|
+
'license-guard', 'prism-engine', 'model-governance',
|
|
358
|
+
'read-protection', 'constitution-guard', 'mode-governance',
|
|
359
|
+
'style-guard', 'undercover-guard', 'team-quality-gate', 'session-digest',
|
|
360
|
+
];
|
|
361
|
+
const missingHooks = requiredHooks.filter(h => !allCommands.some(c => c.includes(h)));
|
|
362
|
+
if (missingHooks.length > 0) {
|
|
363
|
+
results.errors.push(`generateClaudeSettings missing hook references: ${missingHooks.join(', ')}`);
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Verify each referenced hook file exists in framework/hooks/
|
|
368
|
+
const hooksDir = join(packageRoot, 'framework', 'hooks');
|
|
369
|
+
if (existsSync(hooksDir)) {
|
|
370
|
+
const missingFiles = [];
|
|
371
|
+
for (const cmd of allCommands) {
|
|
372
|
+
const match = cmd.match(/hooks\/([^\s]+\.js)/);
|
|
373
|
+
if (match && !existsSync(join(hooksDir, match[1]))) {
|
|
374
|
+
missingFiles.push(match[1]);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
if (missingFiles.length > 0) {
|
|
378
|
+
results.errors.push(`Claude settings reference hook files missing from bundle: ${missingFiles.join(', ')}`);
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
results.passed++;
|
|
384
|
+
} catch (err) {
|
|
385
|
+
results.errors.push(`checkClaudeHooksWired failed: ${err.message}`);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* v4.2.2: Verify license-guard is wired for ALL three providers.
|
|
391
|
+
* Without this, an expired trial keeps working forever — exactly the bug
|
|
392
|
+
* the user flagged in v4.2.1 (they could see chati.dev with no license check).
|
|
393
|
+
*/
|
|
394
|
+
async function checkLicenseEntryPointAcrossProviders(packageRoot, results) {
|
|
395
|
+
results.checks++;
|
|
396
|
+
try {
|
|
397
|
+
const missing = [];
|
|
398
|
+
|
|
399
|
+
// Claude — settings.json must include license-guard in UserPromptSubmit
|
|
400
|
+
const genPath = join(packageRoot, 'src', 'config', 'claude-settings-generator.js');
|
|
401
|
+
if (existsSync(genPath)) {
|
|
402
|
+
const mod = await import(genPath);
|
|
403
|
+
const settings = JSON.parse(mod.generateClaudeSettings());
|
|
404
|
+
const ups = settings.hooks?.UserPromptSubmit || [];
|
|
405
|
+
const claudeCmds = ups.flatMap(g => (g.hooks || []).map(h => h.command || ''));
|
|
406
|
+
if (!claudeCmds.some(c => c.includes('license-guard'))) {
|
|
407
|
+
missing.push('Claude (UserPromptSubmit)');
|
|
408
|
+
}
|
|
409
|
+
} else {
|
|
410
|
+
missing.push('Claude (generator file missing)');
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Gemini — HOOK_MAP must include license-guard at BeforeModel
|
|
414
|
+
const geminiPath = join(packageRoot, 'src', 'config', 'gemini-hooks-generator.js');
|
|
415
|
+
if (existsSync(geminiPath)) {
|
|
416
|
+
const mod = await import(geminiPath);
|
|
417
|
+
if (!mod.HOOK_MAP?.['license-guard'] || mod.HOOK_MAP['license-guard'].event !== 'BeforeModel') {
|
|
418
|
+
missing.push('Gemini (HOOK_MAP missing license-guard at BeforeModel)');
|
|
419
|
+
}
|
|
420
|
+
} else {
|
|
421
|
+
missing.push('Gemini (generator file missing)');
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Codex — installer/core.js must write .codex/hooks.json with license-guard
|
|
425
|
+
const corePath = join(packageRoot, 'src', 'installer', 'core.js');
|
|
426
|
+
if (existsSync(corePath)) {
|
|
427
|
+
const coreContent = readFileSync(corePath, 'utf-8');
|
|
428
|
+
if (!coreContent.includes('.codex/hooks.json') || !coreContent.includes("license-guard")) {
|
|
429
|
+
missing.push('Codex (.codex/hooks.json not wired with license-guard in core.js)');
|
|
430
|
+
}
|
|
431
|
+
} else {
|
|
432
|
+
missing.push('Codex (core.js missing)');
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if (missing.length > 0) {
|
|
436
|
+
results.errors.push(`License enforcement gaps: ${missing.join('; ')}`);
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
results.passed++;
|
|
441
|
+
} catch (err) {
|
|
442
|
+
results.errors.push(`checkLicenseEntryPointAcrossProviders failed: ${err.message}`);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
204
446
|
/**
|
|
205
447
|
* Validate the npm package completeness.
|
|
206
448
|
* @param {string} packageRoot - Root of the package (packages/chati-dev/)
|
|
207
|
-
* @returns {{ errors: string[], warnings: string[], checks: number, passed: number }}
|
|
449
|
+
* @returns {Promise<{ errors: string[], warnings: string[], checks: number, passed: number }>}
|
|
208
450
|
*/
|
|
209
|
-
export function validatePackage(packageRoot) {
|
|
451
|
+
export async function validatePackage(packageRoot) {
|
|
210
452
|
const results = { errors: [], warnings: [], checks: 0, passed: 0 };
|
|
211
453
|
|
|
212
454
|
checkFrameworkExists(packageRoot, results);
|
|
@@ -216,6 +458,10 @@ export function validatePackage(packageRoot) {
|
|
|
216
458
|
checkExportsResolve(packageRoot, results);
|
|
217
459
|
checkBinExists(packageRoot, results);
|
|
218
460
|
checkEntityCount(packageRoot, results);
|
|
461
|
+
checkHooksParity(packageRoot, results);
|
|
462
|
+
checkSourceBundleParity(packageRoot, results);
|
|
463
|
+
await checkClaudeHooksWired(packageRoot, results);
|
|
464
|
+
await checkLicenseEntryPointAcrossProviders(packageRoot, results);
|
|
219
465
|
|
|
220
466
|
return results;
|
|
221
467
|
}
|
|
@@ -247,11 +493,13 @@ function formatResults(results) {
|
|
|
247
493
|
// CLI entry point
|
|
248
494
|
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
249
495
|
const packageRoot = join(__dirname, '..');
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
496
|
+
validatePackage(packageRoot).then(results => {
|
|
497
|
+
console.log(formatResults(results));
|
|
498
|
+
if (results.errors.length > 0) {
|
|
499
|
+
process.exit(1);
|
|
500
|
+
}
|
|
501
|
+
}).catch(err => {
|
|
502
|
+
console.error('validate-package failed:', err);
|
|
255
503
|
process.exit(1);
|
|
256
|
-
}
|
|
504
|
+
});
|
|
257
505
|
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code settings.json generator.
|
|
3
|
+
*
|
|
4
|
+
* Produces .claude/settings.json content that wires the 10 chati.dev hooks
|
|
5
|
+
* into Claude Code's hook system and applies a baseline permission policy.
|
|
6
|
+
*
|
|
7
|
+
* Background: prior to v4.2.2, the installer wrote hooks to
|
|
8
|
+
* chati.dev/hooks/settings.json — but Claude Code reads .claude/settings.json,
|
|
9
|
+
* not the framework's bundled file. As a result, every hook was dormant for
|
|
10
|
+
* every Claude Code user since the project started. This generator is the fix:
|
|
11
|
+
* it produces the file Claude Code actually loads, registers all 10 hooks at
|
|
12
|
+
* the canonical events, and applies a generic permission policy modeled on
|
|
13
|
+
* production Claude Code setups.
|
|
14
|
+
*
|
|
15
|
+
* Hook output schema (v4.2.2): all PreToolUse hooks now use the canonical
|
|
16
|
+
* `hookSpecificOutput.permissionDecision` field per Anthropic docs at
|
|
17
|
+
* https://code.claude.com/docs/en/hooks.md. The previous `decision` field
|
|
18
|
+
* was silently no-op'd by Claude Code.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Hook registration map. Each entry is one hook script and the events it
|
|
23
|
+
* should fire on. Source of truth lives in chati.dev/hooks/.
|
|
24
|
+
*/
|
|
25
|
+
const HOOK_REGISTRY = [
|
|
26
|
+
// UserPromptSubmit — fires on every user message turn.
|
|
27
|
+
// license-guard MUST be first so license check runs before any other work.
|
|
28
|
+
{ name: 'license-guard', event: 'UserPromptSubmit', matcher: '.*' },
|
|
29
|
+
{ name: 'prism-engine', event: 'UserPromptSubmit', matcher: '.*' },
|
|
30
|
+
{ name: 'model-governance', event: 'UserPromptSubmit', matcher: '.*' },
|
|
31
|
+
|
|
32
|
+
// PreToolUse — fires before each tool call. Matcher targets specific tools.
|
|
33
|
+
{ name: 'read-protection', event: 'PreToolUse', matcher: 'Read' },
|
|
34
|
+
{ name: 'constitution-guard',event: 'PreToolUse', matcher: 'Bash|Write|Edit' },
|
|
35
|
+
{ name: 'mode-governance', event: 'PreToolUse', matcher: 'Write|Edit' },
|
|
36
|
+
{ name: 'style-guard', event: 'PreToolUse', matcher: 'Write|Edit|Bash' },
|
|
37
|
+
{ name: 'undercover-guard', event: 'PreToolUse', matcher: 'Write|Edit|Bash' },
|
|
38
|
+
{ name: 'team-quality-gate', event: 'PreToolUse', matcher: 'Write|Edit' },
|
|
39
|
+
|
|
40
|
+
// PreCompact — fires before context compaction (observational).
|
|
41
|
+
{ name: 'session-digest', event: 'PreCompact', matcher: '' },
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Baseline permission allow list.
|
|
46
|
+
* Modeled on production Claude Code setups: lets the assistant work freely
|
|
47
|
+
* inside the project without prompting for every tool call. Personal MCP
|
|
48
|
+
* permissions are intentionally NOT included — those are user-specific.
|
|
49
|
+
*/
|
|
50
|
+
const PERMISSIONS_ALLOW = [
|
|
51
|
+
'Read(**/*)',
|
|
52
|
+
'Write(**/*)',
|
|
53
|
+
'Edit(**/*)',
|
|
54
|
+
'Bash',
|
|
55
|
+
'WebFetch',
|
|
56
|
+
'WebSearch',
|
|
57
|
+
'Task',
|
|
58
|
+
'Glob',
|
|
59
|
+
'Grep',
|
|
60
|
+
'NotebookEdit',
|
|
61
|
+
'Skill(*)',
|
|
62
|
+
'TodoWrite',
|
|
63
|
+
'AskUserQuestion',
|
|
64
|
+
'EnterPlanMode',
|
|
65
|
+
'ExitPlanMode',
|
|
66
|
+
'KillShell',
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Baseline permission deny list.
|
|
71
|
+
*
|
|
72
|
+
* Categories:
|
|
73
|
+
* - Catastrophic filesystem: deletes that hit root, mass-format, recursive chmod
|
|
74
|
+
* - Git destructive: force push, hard reset, .git removal, hook bypass
|
|
75
|
+
* - System path writes: /etc, /usr, /System, /bin, /sbin (note `//` prefix
|
|
76
|
+
* means absolute filesystem path, not project-relative)
|
|
77
|
+
* - Secret reads: SSH keys, AWS/GCP credentials, npm tokens, GitHub tokens
|
|
78
|
+
* - Accidental publish: npm/yarn/pnpm/pip/cargo publish commands
|
|
79
|
+
*
|
|
80
|
+
* Path syntax (from Claude Code docs):
|
|
81
|
+
* - `Bash(cmd:*)` matches "cmd" with any args
|
|
82
|
+
* - `~/path/**` expands ~ to home dir, ** matches recursive
|
|
83
|
+
* - `//etc/**` double-slash means absolute filesystem path
|
|
84
|
+
* - `/path/**` single-slash is project-root-relative (NOT absolute)
|
|
85
|
+
*/
|
|
86
|
+
const PERMISSIONS_DENY = [
|
|
87
|
+
// Catastrophic filesystem
|
|
88
|
+
'Bash(rm -rf /)',
|
|
89
|
+
'Bash(rm -rf /*)',
|
|
90
|
+
'Bash(rm -rf ~)',
|
|
91
|
+
'Bash(rm -rf ~/*)',
|
|
92
|
+
'Bash(rm -rf $HOME)',
|
|
93
|
+
'Bash(rm -rf $HOME/*)',
|
|
94
|
+
'Bash(sudo rm -rf:*)',
|
|
95
|
+
'Bash(mkfs:*)',
|
|
96
|
+
'Bash(dd if=/dev/zero:*)',
|
|
97
|
+
'Bash(dd if=/dev/random:*)',
|
|
98
|
+
'Bash(chmod -R 777 /)',
|
|
99
|
+
'Bash(chown -R:* /)',
|
|
100
|
+
|
|
101
|
+
// Git destructive
|
|
102
|
+
'Bash(git push --force:*)',
|
|
103
|
+
'Bash(git push -f:*)',
|
|
104
|
+
'Bash(git push --force-with-lease:*)',
|
|
105
|
+
'Bash(git reset --hard:*)',
|
|
106
|
+
'Bash(git clean -fd:*)',
|
|
107
|
+
'Bash(git clean -fdx:*)',
|
|
108
|
+
'Bash(git clean -fx:*)',
|
|
109
|
+
'Bash(rm -rf .git)',
|
|
110
|
+
'Bash(rm -rf .git/*)',
|
|
111
|
+
|
|
112
|
+
// Bypass hooks (always suspect)
|
|
113
|
+
'Bash(git commit --no-verify:*)',
|
|
114
|
+
'Bash(git commit -n:*)',
|
|
115
|
+
'Bash(git commit --no-gpg-sign:*)',
|
|
116
|
+
'Bash(git rebase --no-verify:*)',
|
|
117
|
+
|
|
118
|
+
// System path writes — `//` prefix = absolute filesystem
|
|
119
|
+
'Write(//etc/**)',
|
|
120
|
+
'Write(//usr/**)',
|
|
121
|
+
'Write(//System/**)',
|
|
122
|
+
'Write(//bin/**)',
|
|
123
|
+
'Write(//sbin/**)',
|
|
124
|
+
'Write(//Library/**)',
|
|
125
|
+
'Edit(//etc/**)',
|
|
126
|
+
'Edit(//usr/**)',
|
|
127
|
+
'Edit(//System/**)',
|
|
128
|
+
'Edit(//bin/**)',
|
|
129
|
+
'Edit(//sbin/**)',
|
|
130
|
+
'Edit(//Library/**)',
|
|
131
|
+
|
|
132
|
+
// Credentials and secrets
|
|
133
|
+
'Read(~/.ssh/**)',
|
|
134
|
+
'Read(~/.aws/credentials)',
|
|
135
|
+
'Read(~/.aws/config)',
|
|
136
|
+
'Read(~/.gnupg/**)',
|
|
137
|
+
'Read(~/.config/gh/hosts.yml)',
|
|
138
|
+
'Read(~/.docker/config.json)',
|
|
139
|
+
'Read(~/.npmrc)',
|
|
140
|
+
'Read(~/.pypirc)',
|
|
141
|
+
'Read(~/.netrc)',
|
|
142
|
+
|
|
143
|
+
// Accidental publish
|
|
144
|
+
'Bash(npm publish:*)',
|
|
145
|
+
'Bash(yarn publish:*)',
|
|
146
|
+
'Bash(pnpm publish:*)',
|
|
147
|
+
'Bash(pip upload:*)',
|
|
148
|
+
'Bash(twine upload:*)',
|
|
149
|
+
'Bash(cargo publish:*)',
|
|
150
|
+
];
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Group HOOK_REGISTRY entries by event, then by matcher within each event.
|
|
154
|
+
* Returns the shape Claude Code expects under the top-level "hooks" key.
|
|
155
|
+
*/
|
|
156
|
+
function buildHooksObject() {
|
|
157
|
+
const events = {};
|
|
158
|
+
|
|
159
|
+
for (const entry of HOOK_REGISTRY) {
|
|
160
|
+
if (!events[entry.event]) events[entry.event] = new Map();
|
|
161
|
+
const matcherGroups = events[entry.event];
|
|
162
|
+
if (!matcherGroups.has(entry.matcher)) {
|
|
163
|
+
matcherGroups.set(entry.matcher, []);
|
|
164
|
+
}
|
|
165
|
+
matcherGroups.get(entry.matcher).push({
|
|
166
|
+
type: 'command',
|
|
167
|
+
command: `node chati.dev/hooks/${entry.name}.js`,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const result = {};
|
|
172
|
+
for (const [event, matcherGroups] of Object.entries(events)) {
|
|
173
|
+
result[event] = [];
|
|
174
|
+
for (const [matcher, hooks] of matcherGroups.entries()) {
|
|
175
|
+
const group = { hooks };
|
|
176
|
+
if (matcher) group.matcher = matcher;
|
|
177
|
+
result[event].push(group);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return result;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Generate the full settings.json content.
|
|
185
|
+
*
|
|
186
|
+
* @returns {string} JSON string with trailing newline, ready to write to disk.
|
|
187
|
+
*/
|
|
188
|
+
export function generateClaudeSettings() {
|
|
189
|
+
const settings = {
|
|
190
|
+
$schema: 'https://json.schemastore.org/claude-code-settings.json',
|
|
191
|
+
permissions: {
|
|
192
|
+
allow: PERMISSIONS_ALLOW,
|
|
193
|
+
deny: PERMISSIONS_DENY,
|
|
194
|
+
defaultMode: 'default',
|
|
195
|
+
},
|
|
196
|
+
hooks: buildHooksObject(),
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
return JSON.stringify(settings, null, 2) + '\n';
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export {
|
|
203
|
+
HOOK_REGISTRY,
|
|
204
|
+
PERMISSIONS_ALLOW,
|
|
205
|
+
PERMISSIONS_DENY,
|
|
206
|
+
};
|