chati-dev 4.5.6 → 4.5.8
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/agents/build/dev.md +1 -1
- package/framework/agents/deploy/devops.md +1 -1
- package/framework/agents/discover/brief.md +1 -1
- package/framework/agents/discover/brownfield-wu.md +1 -1
- package/framework/agents/discover/greenfield-wu.md +1 -1
- package/framework/agents/plan/architect-data-engineer.md +1 -1
- package/framework/agents/plan/architect-system.md +1 -1
- package/framework/agents/plan/architect.md +1 -1
- package/framework/agents/plan/detail.md +1 -1
- package/framework/agents/plan/phases.md +1 -1
- package/framework/agents/plan/tasks.md +1 -1
- package/framework/agents/plan/ux-brand-architect.md +1 -1
- package/framework/agents/plan/ux-component-engineer.md +1 -1
- package/framework/agents/plan/ux-researcher.md +1 -1
- package/framework/agents/plan/ux.md +1 -1
- package/framework/agents/quality/qa-implementation.md +1 -1
- package/framework/agents/quality/qa-planning.md +1 -1
- package/framework/agents/quality/qa-visual.md +1 -1
- package/framework/agents/shared/visualizer.md +1 -1
- package/framework/config.yaml +2 -2
- package/framework/context/root.md +1 -1
- package/framework/data/entity-registry.yaml +1 -1
- package/framework/hooks/license-guard.js +19 -0
- package/framework/manifest.json +55 -50
- package/framework/manifest.sig +1 -1
- package/framework/orchestrator/chati-router.js +177 -71
- package/framework/orchestrator/chati.md +1 -1
- package/framework/package.json +3 -0
- package/framework/schemas/session.schema.json +26 -9
- package/package.json +2 -1
- package/src/config/claude-settings-generator.js +6 -6
- package/src/installer/core.js +55 -25
- package/src/installer/path-replacement.js +13 -0
- package/src/installer/provider-overlay.js +3 -1
- package/src/installer-v2/index.js +55 -0
- package/src/orchestrator/cli.js +9 -6
- package/src/orchestrator/session-manager.js +134 -96
- package/src/terminal/adapters/claude-adapter.js +9 -2
- package/src/terminal/adapters/codex-adapter.js +9 -2
- package/src/terminal/adapters/gemini-adapter.js +5 -2
- package/src/terminal/adapters/grok-adapter.js +13 -3
- package/src/terminal/cli-registry.js +5 -0
- package/src/terminal/run-agent.js +5 -0
- package/src/terminal/run-parallel.js +7 -0
- package/src/terminal/spawner.js +49 -10
|
@@ -21,6 +21,125 @@ import { join } from 'path';
|
|
|
21
21
|
const PROJECT_DIR = process.cwd();
|
|
22
22
|
const ROUTER_ARGS = process.argv.slice(2);
|
|
23
23
|
|
|
24
|
+
async function readEnabledHarnesses(projectDir) {
|
|
25
|
+
const installationPath = join(projectDir, '.chati', 'v2', 'installation.json');
|
|
26
|
+
if (existsSync(installationPath)) {
|
|
27
|
+
try {
|
|
28
|
+
const artifact = JSON.parse(readFileSync(installationPath, 'utf-8'));
|
|
29
|
+
const fwDir = existsSync(join(projectDir, '.chati.dev')) ? '.chati.dev' : 'chati.dev';
|
|
30
|
+
const doctorModulePath = join(projectDir, fwDir, '_cli', 'installer-v2', 'index.js');
|
|
31
|
+
if (!existsSync(doctorModulePath)) return { harnesses: [], error: 'v2 doctor missing' };
|
|
32
|
+
const { doctorV2 } = await import(doctorModulePath);
|
|
33
|
+
if (!doctorV2(artifact).passed) return { harnesses: [], error: 'v2 artifact invalid' };
|
|
34
|
+
const harnesses = artifact.installation?.enabled_providers
|
|
35
|
+
?.map((binding) => binding.harness_id)
|
|
36
|
+
.filter(Boolean);
|
|
37
|
+
if (harnesses?.length) return { harnesses: [...new Set(harnesses)], error: null };
|
|
38
|
+
} catch { return { harnesses: [], error: 'v2 artifact invalid' }; }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const detected = [];
|
|
42
|
+
if (existsSync(join(projectDir, '.claude', 'commands', 'chati.md'))) detected.push('claude');
|
|
43
|
+
if (existsSync(join(projectDir, '.agents', 'skills', 'chati', 'SKILL.md'))) detected.push('codex');
|
|
44
|
+
if (existsSync(join(projectDir, '.grok', 'commands', 'chati.md'))) detected.push('grok');
|
|
45
|
+
return { harnesses: detected, error: null };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function hasContent(path) {
|
|
49
|
+
return existsSync(path) && readFileSync(path, 'utf-8').trim().length > 0;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function codexHooksEnabled(config) {
|
|
53
|
+
if (/^\s*features\.hooks\s*=\s*true\s*(?:#.*)?$/m.test(config)) return true;
|
|
54
|
+
const lines = config.split('\n');
|
|
55
|
+
const sectionStart = lines.findIndex((line) => /^\s*\[features\]\s*(?:#.*)?$/.test(line));
|
|
56
|
+
if (sectionStart === -1) return false;
|
|
57
|
+
const sectionEnd = lines.findIndex((line, index) => index > sectionStart && /^\s*\[.*\]\s*(?:#.*)?$/.test(line));
|
|
58
|
+
const end = sectionEnd === -1 ? lines.length : sectionEnd;
|
|
59
|
+
return lines.slice(sectionStart + 1, end).some((line) => /^\s*hooks\s*=\s*true\s*(?:#.*)?$/.test(line));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function validateProviderIntegrity(projectDir) {
|
|
63
|
+
const missing = [];
|
|
64
|
+
const { harnesses, error } = await readEnabledHarnesses(projectDir);
|
|
65
|
+
|
|
66
|
+
if (error) missing.push(error);
|
|
67
|
+
const supportedHarnesses = new Set(['claude', 'codex', 'grok']);
|
|
68
|
+
for (const harness of harnesses) {
|
|
69
|
+
if (!supportedHarnesses.has(harness)) missing.push(`unsupported harness ${harness}`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (harnesses.length === 0) missing.push('provider entry point');
|
|
73
|
+
|
|
74
|
+
if (harnesses.includes('claude')) {
|
|
75
|
+
const commandPath = join(projectDir, '.claude', 'commands', 'chati.md');
|
|
76
|
+
const settingsPath = join(projectDir, '.claude', 'settings.json');
|
|
77
|
+
if (!hasContent(commandPath)) missing.push('claude command');
|
|
78
|
+
if (!existsSync(settingsPath)) {
|
|
79
|
+
missing.push('claude settings');
|
|
80
|
+
} else {
|
|
81
|
+
try {
|
|
82
|
+
const settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
|
|
83
|
+
const ptu = settings.hooks?.PreToolUse || [];
|
|
84
|
+
const ups = settings.hooks?.UserPromptSubmit || [];
|
|
85
|
+
const ptuCmds = ptu.flatMap((group) => (group.hooks || []).map((hook) => hook.command || ''));
|
|
86
|
+
const upsCmds = ups.flatMap((group) => (group.hooks || []).map((hook) => hook.command || ''));
|
|
87
|
+
missing.push(...['mode-governance', 'constitution-guard', 'read-protection']
|
|
88
|
+
.filter((hook) => !ptuCmds.some((command) => command.includes(hook)))
|
|
89
|
+
.map((hook) => `claude hook ${hook}`));
|
|
90
|
+
if (![...upsCmds, ...ptuCmds].some((command) => command.includes('license-guard'))) {
|
|
91
|
+
missing.push('claude hook license-guard');
|
|
92
|
+
}
|
|
93
|
+
} catch {
|
|
94
|
+
missing.push('claude settings invalid');
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (harnesses.includes('codex')) {
|
|
100
|
+
const required = [
|
|
101
|
+
['codex skill', '.agents/skills/chati/SKILL.md'],
|
|
102
|
+
['codex constitution guard', '.codex/rules/constitution-guard.rules'],
|
|
103
|
+
['codex read protection', '.codex/rules/read-protection.rules'],
|
|
104
|
+
];
|
|
105
|
+
for (const [label, relativePath] of required) {
|
|
106
|
+
if (!hasContent(join(projectDir, relativePath))) missing.push(label);
|
|
107
|
+
}
|
|
108
|
+
const configPath = join(projectDir, '.codex', 'config.toml');
|
|
109
|
+
if (!hasContent(configPath) || !codexHooksEnabled(readFileSync(configPath, 'utf-8'))) {
|
|
110
|
+
missing.push('codex hooks feature');
|
|
111
|
+
}
|
|
112
|
+
const hooksPath = join(projectDir, '.codex', 'hooks.json');
|
|
113
|
+
if (!hasContent(hooksPath)) {
|
|
114
|
+
missing.push('codex license hook');
|
|
115
|
+
} else {
|
|
116
|
+
try {
|
|
117
|
+
const hooks = JSON.parse(readFileSync(hooksPath, 'utf-8'));
|
|
118
|
+
const validHook = (hooks.hooks?.UserPromptSubmit || []).some((group) =>
|
|
119
|
+
group.matcher === '.*'
|
|
120
|
+
&& (group.hooks || []).some((hook) =>
|
|
121
|
+
hook.type === 'command'
|
|
122
|
+
&& hook.command === 'node .chati.dev/hooks/license-guard.js'
|
|
123
|
+
&& hook.async !== true
|
|
124
|
+
)
|
|
125
|
+
);
|
|
126
|
+
if (!validHook) {
|
|
127
|
+
missing.push('codex license hook');
|
|
128
|
+
}
|
|
129
|
+
} catch {
|
|
130
|
+
missing.push('codex hooks invalid');
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (harnesses.includes('grok')) {
|
|
136
|
+
if (!hasContent(join(projectDir, '.grok', 'commands', 'chati.md'))) missing.push('grok command');
|
|
137
|
+
if (!hasContent(join(projectDir, '.grok', 'session-lock.md'))) missing.push('grok session lock');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return { valid: missing.length === 0, missing };
|
|
141
|
+
}
|
|
142
|
+
|
|
24
143
|
/**
|
|
25
144
|
* Find the chati-dev CLI module. Tries local node_modules first (fast),
|
|
26
145
|
* then monorepo dev layout. Returns the module or null.
|
|
@@ -245,9 +364,64 @@ async function dispatchOrchestrate(subCommand, argv) {
|
|
|
245
364
|
}
|
|
246
365
|
}
|
|
247
366
|
|
|
367
|
+
async function runSecurityPreflight(projectDir) {
|
|
368
|
+
const result = { ok: true, license: null, integrity: true, error: null };
|
|
369
|
+
try {
|
|
370
|
+
const integrity = await validateProviderIntegrity(projectDir);
|
|
371
|
+
if (!integrity.valid) {
|
|
372
|
+
return {
|
|
373
|
+
...result,
|
|
374
|
+
ok: false,
|
|
375
|
+
integrity: false,
|
|
376
|
+
error: `integrity_failed: missing ${integrity.missing.join(', ')}`,
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
} catch {
|
|
380
|
+
return { ...result, ok: false, integrity: false, error: 'integrity_failed' };
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
try {
|
|
384
|
+
const fwDir = existsSync(join(projectDir, '.chati.dev')) ? '.chati.dev' : 'chati.dev';
|
|
385
|
+
const hookPath = join(projectDir, fwDir, 'hooks', 'license-guard.js');
|
|
386
|
+
if (!existsSync(hookPath)) {
|
|
387
|
+
return {
|
|
388
|
+
...result,
|
|
389
|
+
ok: false,
|
|
390
|
+
error: 'license_guard_missing',
|
|
391
|
+
license: { valid: false, reason: 'License enforcement file missing. Reinstall with: npx chati-dev init' },
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
const mod = await import(hookPath);
|
|
395
|
+
result.license = await mod.checkLicense();
|
|
396
|
+
if (!result.license.valid) return { ...result, ok: false, error: 'license_invalid' };
|
|
397
|
+
} catch {
|
|
398
|
+
return {
|
|
399
|
+
...result,
|
|
400
|
+
ok: false,
|
|
401
|
+
error: 'license_check_error',
|
|
402
|
+
license: { valid: false, error: 'license_check_error' },
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
return result;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Only read-only diagnosis and license recovery may run without a valid
|
|
410
|
+
// installation preflight. `init` mutates session state and must never bypass
|
|
411
|
+
// license or provider-integrity enforcement.
|
|
412
|
+
const PREFLIGHT_EXEMPT_SUBS = new Set(['doctor', 'wait-for-license']);
|
|
413
|
+
|
|
248
414
|
async function main() {
|
|
249
415
|
const subCommand = ROUTER_ARGS[0];
|
|
250
416
|
|
|
417
|
+
const preflight = subCommand && PREFLIGHT_EXEMPT_SUBS.has(subCommand)
|
|
418
|
+
? { ok: true, license: null, integrity: true, error: null }
|
|
419
|
+
: await runSecurityPreflight(PROJECT_DIR);
|
|
420
|
+
if (!preflight.ok) {
|
|
421
|
+
console.log(JSON.stringify(preflight));
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
|
|
251
425
|
// Sub-command mode: `node chati-router.js <subcommand> [--flags...]`
|
|
252
426
|
// Routes to the local CLI for ANY orchestrate subcommand. No npx, no network.
|
|
253
427
|
if (subCommand && VALID_ORCHESTRATE_SUBS.has(subCommand)) {
|
|
@@ -271,8 +445,8 @@ async function main() {
|
|
|
271
445
|
// No-subcommand mode (default): full router pipeline (license + integrity + session + next)
|
|
272
446
|
const result = {
|
|
273
447
|
ok: true,
|
|
274
|
-
license:
|
|
275
|
-
integrity:
|
|
448
|
+
license: preflight.license,
|
|
449
|
+
integrity: preflight.integrity,
|
|
276
450
|
session: null,
|
|
277
451
|
action: null,
|
|
278
452
|
pipeline: null,
|
|
@@ -281,75 +455,7 @@ async function main() {
|
|
|
281
455
|
};
|
|
282
456
|
|
|
283
457
|
// -----------------------------------------------------------------------
|
|
284
|
-
// 1.
|
|
285
|
-
// -----------------------------------------------------------------------
|
|
286
|
-
try {
|
|
287
|
-
const fwDir = existsSync(join(PROJECT_DIR, '.chati.dev')) ? '.chati.dev' : 'chati.dev';
|
|
288
|
-
const hookPath = join(PROJECT_DIR, fwDir, 'hooks', 'license-guard.js');
|
|
289
|
-
if (!existsSync(hookPath)) {
|
|
290
|
-
result.ok = false;
|
|
291
|
-
result.error = 'license_guard_missing';
|
|
292
|
-
result.license = { valid: false, reason: 'License enforcement file missing. Reinstall with: npx chati-dev init' };
|
|
293
|
-
console.log(JSON.stringify(result));
|
|
294
|
-
return;
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
const mod = await import(hookPath);
|
|
298
|
-
const license = await mod.checkLicense();
|
|
299
|
-
result.license = license;
|
|
300
|
-
|
|
301
|
-
if (!license.valid) {
|
|
302
|
-
result.ok = false;
|
|
303
|
-
result.error = 'license_invalid';
|
|
304
|
-
console.log(JSON.stringify(result));
|
|
305
|
-
return;
|
|
306
|
-
}
|
|
307
|
-
} catch (err) {
|
|
308
|
-
// Fail-closed on license check errors
|
|
309
|
-
result.license = { valid: false, error: 'license_check_error' };
|
|
310
|
-
result.ok = false;
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
// -----------------------------------------------------------------------
|
|
314
|
-
// 2. Integrity check — are all required hooks registered in settings.json?
|
|
315
|
-
// Note: license-guard is registered under UserPromptSubmit (not PreToolUse).
|
|
316
|
-
// mode-governance, constitution-guard, and read-protection are in PreToolUse.
|
|
317
|
-
// -----------------------------------------------------------------------
|
|
318
|
-
try {
|
|
319
|
-
const settingsPath = join(PROJECT_DIR, '.claude', 'settings.json');
|
|
320
|
-
if (existsSync(settingsPath)) {
|
|
321
|
-
const settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
|
|
322
|
-
const ptu = settings.hooks?.PreToolUse || [];
|
|
323
|
-
const ups = settings.hooks?.UserPromptSubmit || [];
|
|
324
|
-
const ptuCmds = ptu.flatMap(g => (g.hooks || []).map(h => h.command || ''));
|
|
325
|
-
const upsCmds = ups.flatMap(g => (g.hooks || []).map(h => h.command || ''));
|
|
326
|
-
// license-guard lives in UserPromptSubmit; others live in PreToolUse
|
|
327
|
-
const missingHooks = [
|
|
328
|
-
...(['mode-governance', 'constitution-guard', 'read-protection'].filter(h => !ptuCmds.some(c => c.includes(h)))),
|
|
329
|
-
...(['license-guard'].filter(h => !upsCmds.some(c => c.includes(h)) && !ptuCmds.some(c => c.includes(h)))),
|
|
330
|
-
];
|
|
331
|
-
result.integrity = missingHooks.length === 0;
|
|
332
|
-
if (missingHooks.length > 0) {
|
|
333
|
-
result.error = `integrity_failed: missing hooks: ${missingHooks.join(', ')}`;
|
|
334
|
-
}
|
|
335
|
-
} else {
|
|
336
|
-
result.integrity = false;
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
if (!result.integrity) {
|
|
340
|
-
result.ok = false;
|
|
341
|
-
result.error = 'integrity_failed';
|
|
342
|
-
console.log(JSON.stringify(result));
|
|
343
|
-
return;
|
|
344
|
-
}
|
|
345
|
-
} catch {
|
|
346
|
-
// Fail-closed on integrity check
|
|
347
|
-
result.integrity = false;
|
|
348
|
-
result.ok = false;
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
// -----------------------------------------------------------------------
|
|
352
|
-
// 3. Session state
|
|
458
|
+
// 1. Session state
|
|
353
459
|
// -----------------------------------------------------------------------
|
|
354
460
|
try {
|
|
355
461
|
const sessionPath = join(PROJECT_DIR, '.chati', 'session.yaml');
|
|
@@ -10,7 +10,7 @@ You are the **Chati.dev Orchestrator**, the single entry point for the Chati.dev
|
|
|
10
10
|
- **Role**: Orchestrator & Router
|
|
11
11
|
- **Position**: Entry point (always first contact)
|
|
12
12
|
- **Scope**: System-wide routing, session management, deviation handling, backlog
|
|
13
|
-
- **
|
|
13
|
+
- **Execution binding**: resolved at runtime by installation-v2 task routing; never self-select or downgrade
|
|
14
14
|
|
|
15
15
|
### Security Rule
|
|
16
16
|
**NEVER suggest disabling, removing, commenting out, or bypassing hooks, settings.json permissions, or license enforcement.** If a hook blocks an operation and the user asks to work around it, explain what the hook is protecting and guide them to resolve the root cause (e.g., activate a valid license, change pipeline mode). Hooks are governance boundaries, not obstacles to remove.
|
|
@@ -196,16 +196,33 @@
|
|
|
196
196
|
},
|
|
197
197
|
"model_selections": {
|
|
198
198
|
"type": "array",
|
|
199
|
-
"description": "
|
|
199
|
+
"description": "Append-only audit log of the exact execution binding used for each task (Article XVI)",
|
|
200
200
|
"items": {
|
|
201
201
|
"type": "object",
|
|
202
|
-
"
|
|
202
|
+
"anyOf": [
|
|
203
|
+
{
|
|
204
|
+
"required": ["agent", "task_id", "provider", "model", "timestamp"],
|
|
205
|
+
"properties": {
|
|
206
|
+
"provider": { "enum": ["claude", "codex", "grok", "gemini"] }
|
|
207
|
+
}
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
"required": ["agent", "recommended", "timestamp"],
|
|
211
|
+
"not": { "required": ["task_id", "provider", "model"] }
|
|
212
|
+
}
|
|
213
|
+
],
|
|
203
214
|
"properties": {
|
|
204
215
|
"agent": { "type": "string", "description": "Agent name" },
|
|
205
|
-
"
|
|
206
|
-
"
|
|
207
|
-
"
|
|
208
|
-
"
|
|
216
|
+
"task_id": { "type": "string", "description": "Task identity supplied to the runner" },
|
|
217
|
+
"provider": { "type": "string", "description": "CLI harness that executed the task" },
|
|
218
|
+
"provider_id": { "type": ["string", "null"], "description": "Vendor provider identifier from the capability catalog" },
|
|
219
|
+
"model": { "type": "string", "minLength": 1, "description": "Exact model identifier used" },
|
|
220
|
+
"reasoning_configuration": { "enum": ["low", "medium", "high", "xhigh", "max", null], "description": "Reasoning effort passed to the CLI when supported" },
|
|
221
|
+
"catalog_snapshot_ref": { "type": ["string", "null"], "description": "Capability catalog snapshot used for routing" },
|
|
222
|
+
"status": { "enum": ["dispatched", "completed", "failed"], "default": "dispatched" },
|
|
223
|
+
"recommended": { "type": "string", "description": "Legacy pre-4.5.7 recommended model field, migrated on session load" },
|
|
224
|
+
"actual": { "type": "string", "description": "Legacy pre-4.5.7 actual model field, migrated on session load" },
|
|
225
|
+
"reason": { "type": "string", "description": "Legacy pre-4.5.7 selection reason" },
|
|
209
226
|
"timestamp": { "type": "string", "format": "date-time" }
|
|
210
227
|
}
|
|
211
228
|
}
|
|
@@ -367,13 +384,13 @@
|
|
|
367
384
|
"active_model": {
|
|
368
385
|
"type": ["string", "null"],
|
|
369
386
|
"default": null,
|
|
370
|
-
"description": "
|
|
387
|
+
"description": "Model identifier detected for the current interactive harness. Written at /chati boot and used for context-window resolution. Per-task routed models are recorded separately in model_selections[]."
|
|
371
388
|
},
|
|
372
389
|
"active_provider": {
|
|
373
390
|
"type": ["string", "null"],
|
|
374
|
-
"enum": ["claude", "gemini", "codex", null],
|
|
391
|
+
"enum": ["claude", "gemini", "codex", "grok", null],
|
|
375
392
|
"default": null,
|
|
376
|
-
"description": "
|
|
393
|
+
"description": "Current interactive CLI harness. This is independent from the per-task provider recorded in model_selections[]."
|
|
377
394
|
},
|
|
378
395
|
"context_tokens_used": {
|
|
379
396
|
"type": "integer",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chati-dev",
|
|
3
|
-
"version": "4.5.
|
|
3
|
+
"version": "4.5.8",
|
|
4
4
|
"description": "AI-Powered Multi-Agent Orchestration System - Structured vibe coding for Full Stack Development",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -77,6 +77,7 @@
|
|
|
77
77
|
"chalk": "^5.3.0",
|
|
78
78
|
"js-yaml": "^5.4.0",
|
|
79
79
|
"ora": "^8.0.1",
|
|
80
|
+
"proper-lockfile": "4.1.2",
|
|
80
81
|
"semver": "^7.6.0"
|
|
81
82
|
},
|
|
82
83
|
"bundleDependencies": [
|
|
@@ -139,12 +139,12 @@ const PERMISSIONS_DENY = [
|
|
|
139
139
|
'Bash(git rebase --no-verify:*)',
|
|
140
140
|
|
|
141
141
|
// System path writes — `//` prefix = absolute filesystem
|
|
142
|
-
'
|
|
143
|
-
'
|
|
144
|
-
'
|
|
145
|
-
'
|
|
146
|
-
'
|
|
147
|
-
'
|
|
142
|
+
'Edit(//etc/**)',
|
|
143
|
+
'Edit(//usr/**)',
|
|
144
|
+
'Edit(//System/**)',
|
|
145
|
+
'Edit(//bin/**)',
|
|
146
|
+
'Edit(//sbin/**)',
|
|
147
|
+
'Edit(//Library/**)',
|
|
148
148
|
'Edit(//etc/**)',
|
|
149
149
|
'Edit(//usr/**)',
|
|
150
150
|
'Edit(//System/**)',
|
package/src/installer/core.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { mkdirSync, writeFileSync, copyFileSync, existsSync, readFileSync, readdirSync, statSync } from 'fs';
|
|
2
2
|
import { execFileSync } from 'child_process';
|
|
3
|
-
import { join, dirname
|
|
3
|
+
import { join, dirname } from 'path';
|
|
4
4
|
import { fileURLToPath } from 'url';
|
|
5
5
|
import { createRequire } from 'module';
|
|
6
6
|
import { IDE_CONFIGS, IDE_TO_PROVIDER } from '../config/ide-configs.js';
|
|
@@ -10,6 +10,7 @@ import { generateContextFiles } from '../config/context-file-generator.js';
|
|
|
10
10
|
import { adaptFrameworkFile, ADAPTABLE_FILES } from '../config/framework-adapter.js';
|
|
11
11
|
import { generateClaudeSettings } from '../config/claude-settings-generator.js';
|
|
12
12
|
import { generateProviderOverlays } from './provider-overlay.js';
|
|
13
|
+
import { applyInstalledPathReplacement } from './path-replacement.js';
|
|
13
14
|
import { verifyManifest } from './manifest.js';
|
|
14
15
|
|
|
15
16
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
@@ -192,7 +193,7 @@ export async function installFramework(config) {
|
|
|
192
193
|
if (existsSync(src)) {
|
|
193
194
|
// Apply path replacement so rules reference .chati.dev/ not chati.dev/
|
|
194
195
|
let content = readFileSync(src, 'utf-8');
|
|
195
|
-
content =
|
|
196
|
+
content = applyInstalledPathReplacement(content);
|
|
196
197
|
writeFileSync(join(claudeRulesDir, file), content, 'utf-8');
|
|
197
198
|
}
|
|
198
199
|
}
|
|
@@ -269,6 +270,7 @@ export const FRAMEWORK_DIRS_TO_COPY = [
|
|
|
269
270
|
*/
|
|
270
271
|
const FRAMEWORK_ROOT_FILES = [
|
|
271
272
|
'constitution.md',
|
|
273
|
+
'package.json',
|
|
272
274
|
];
|
|
273
275
|
|
|
274
276
|
/**
|
|
@@ -287,20 +289,6 @@ const TEXT_EXTENSIONS = new Set(['.md', '.js', '.yaml', '.yml', '.json', '.html'
|
|
|
287
289
|
* Order matters: replace `chati.dev/artifacts/` FIRST (→ `artifacts/`),
|
|
288
290
|
* then `chati.dev/` (→ `.chati.dev/`). Reversing would produce `.chati.dev/artifacts/`.
|
|
289
291
|
*/
|
|
290
|
-
function applyPathReplacement(content) {
|
|
291
|
-
return content
|
|
292
|
-
// Step 1: artifacts at root (must be before generic replace)
|
|
293
|
-
.replace(/chati\.dev\/artifacts\//g, 'artifacts/')
|
|
294
|
-
// Step 2: path strings with slash — but NOT if already prefixed with dot
|
|
295
|
-
// (negative lookbehind for any char that would create `.chati.dev` already)
|
|
296
|
-
.replace(/(?<!\.)chati\.dev\//g, '.chati.dev/')
|
|
297
|
-
// Step 3: quoted dir name in JS join() calls (WITHOUT slash)
|
|
298
|
-
// Only match 'chati.dev' (the DIRECTORY, with dot) not 'chati-dev' (the NPM PACKAGE, with dash)
|
|
299
|
-
// Also: avoid double-prefixing if already '.chati.dev' in source
|
|
300
|
-
.replace(/(?<!\.)'chati\.dev'/g, "'.chati.dev'")
|
|
301
|
-
.replace(/(?<!\.)"chati\.dev"/g, '".chati.dev"');
|
|
302
|
-
}
|
|
303
|
-
|
|
304
292
|
/**
|
|
305
293
|
* Recursively copy a directory from source to destination, applying:
|
|
306
294
|
* 1. Path replacement (chati.dev/ → .chati.dev/, artifacts/ at root)
|
|
@@ -327,7 +315,7 @@ function copyDirRecursive(srcDir, destDir, provider, relBase = '') {
|
|
|
327
315
|
if (isText) {
|
|
328
316
|
let content = readFileSync(srcPath, 'utf-8');
|
|
329
317
|
// Always apply path replacement for installed location
|
|
330
|
-
content =
|
|
318
|
+
content = applyInstalledPathReplacement(content);
|
|
331
319
|
// Provider adaptation on top (for non-Claude)
|
|
332
320
|
if (provider !== 'claude' && ADAPTABLE_FILES.has(relPath)) {
|
|
333
321
|
content = adaptFrameworkFile(content, relPath, provider);
|
|
@@ -360,7 +348,7 @@ export function copyFrameworkFiles(destDir, provider = 'claude') {
|
|
|
360
348
|
const dest = join(destDir, file);
|
|
361
349
|
createDir(dirname(dest));
|
|
362
350
|
let content = readFileSync(src, 'utf-8');
|
|
363
|
-
content =
|
|
351
|
+
content = applyInstalledPathReplacement(content);
|
|
364
352
|
if (provider !== 'claude' && ADAPTABLE_FILES.has(file)) {
|
|
365
353
|
content = adaptFrameworkFile(content, file, provider);
|
|
366
354
|
}
|
|
@@ -448,17 +436,18 @@ Parse the JSON output. This single command already handled: license validation,
|
|
|
448
436
|
|
|
449
437
|
// Starlark execution policies (Codex sandboxed rule engine)
|
|
450
438
|
createDir(join(targetDir, '.codex', 'rules'));
|
|
439
|
+
writeCodexConfigWithHooks(join(targetDir, '.codex', 'config.toml'));
|
|
451
440
|
writeFileSync(join(targetDir, '.codex', 'rules', 'constitution-guard.rules'), generateCodexConstitutionGuardRules(), 'utf-8');
|
|
452
441
|
writeFileSync(join(targetDir, '.codex', 'rules', 'read-protection.rules'), generateCodexReadProtectionRules(), 'utf-8');
|
|
453
442
|
|
|
454
443
|
// .codex/hooks.json — license-guard wired to UserPromptSubmit (per-turn enforcement).
|
|
455
|
-
// Codex hooks
|
|
456
|
-
//
|
|
444
|
+
// Codex hooks require [features] hooks = true in the active project config.
|
|
445
|
+
// Without it, license enforcement only happens at skill entry and the router,
|
|
457
446
|
// entry — a long-running terminal session would not be re-validated.
|
|
458
447
|
writeFileSync(
|
|
459
448
|
join(targetDir, '.codex', 'hooks.json'),
|
|
460
449
|
JSON.stringify({
|
|
461
|
-
$comment: 'chati.dev
|
|
450
|
+
$comment: 'chati.dev Codex lifecycle hooks. Requires [features] hooks = true.',
|
|
462
451
|
hooks: {
|
|
463
452
|
UserPromptSubmit: [
|
|
464
453
|
{
|
|
@@ -486,7 +475,7 @@ Parse the JSON output. This single command already handled: license validation,
|
|
|
486
475
|
for (const file of contextFileNames) {
|
|
487
476
|
const src = join(FRAMEWORK_SOURCE, 'context', file);
|
|
488
477
|
if (existsSync(src)) {
|
|
489
|
-
const content = readFileSync(src, 'utf-8');
|
|
478
|
+
const content = applyInstalledPathReplacement(readFileSync(src, 'utf-8'));
|
|
490
479
|
writeFileSync(join(geminiContextDir, file), adaptFrameworkFile(content, `context/${file}`, 'gemini'), 'utf-8');
|
|
491
480
|
}
|
|
492
481
|
}
|
|
@@ -515,6 +504,38 @@ Parse the JSON output. This single command already handled: license validation,
|
|
|
515
504
|
}
|
|
516
505
|
}
|
|
517
506
|
|
|
507
|
+
export function writeCodexConfigWithHooks(configPath) {
|
|
508
|
+
createDir(dirname(configPath));
|
|
509
|
+
if (!existsSync(configPath)) {
|
|
510
|
+
writeFileSync(configPath, '[features]\nhooks = true\n', 'utf-8');
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
const original = readFileSync(configPath, 'utf-8');
|
|
515
|
+
if (/^\s*features\.hooks\s*=\s*true\s*(?:#.*)?$/m.test(original)) return;
|
|
516
|
+
if (/^\s*features\.hooks\s*=/m.test(original)) {
|
|
517
|
+
writeFileSync(configPath, original.replace(/^\s*features\.hooks\s*=.*$/m, 'features.hooks = true'), 'utf-8');
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
const lines = original.split('\n');
|
|
522
|
+
const sectionStart = lines.findIndex((line) => /^\s*\[features\]\s*(?:#.*)?$/.test(line));
|
|
523
|
+
if (sectionStart === -1) {
|
|
524
|
+
const prefix = original.endsWith('\n') ? original : `${original}\n`;
|
|
525
|
+
writeFileSync(configPath, `${prefix}\n[features]\nhooks = true\n`, 'utf-8');
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
let sectionEnd = lines.findIndex((line, index) => index > sectionStart && /^\s*\[.*\]\s*(?:#.*)?$/.test(line));
|
|
530
|
+
if (sectionEnd === -1) sectionEnd = lines.length;
|
|
531
|
+
const hookIndex = lines.findIndex((line, index) =>
|
|
532
|
+
index > sectionStart && index < sectionEnd && /^\s*hooks\s*=/.test(line)
|
|
533
|
+
);
|
|
534
|
+
if (hookIndex !== -1) lines[hookIndex] = 'hooks = true';
|
|
535
|
+
else lines.splice(sectionEnd, 0, 'hooks = true');
|
|
536
|
+
writeFileSync(configPath, lines.join('\n'), 'utf-8');
|
|
537
|
+
}
|
|
538
|
+
|
|
518
539
|
/**
|
|
519
540
|
* Generate provider-agnostic instructions file content.
|
|
520
541
|
* Used for non-Claude IDEs (.vscode/chati/rules.md, .cursorrules, etc.)
|
|
@@ -578,6 +599,10 @@ export function updateGitignore(targetDir, selectedIDEs) {
|
|
|
578
599
|
'.chati/memories/*/session/',
|
|
579
600
|
'# NOTE: durable/ and daily/ memories ARE committed — they contain project knowledge',
|
|
580
601
|
'',
|
|
602
|
+
'# Operating system metadata',
|
|
603
|
+
'.DS_Store',
|
|
604
|
+
'._*',
|
|
605
|
+
'',
|
|
581
606
|
'# Claude Code integration (framework portions only)',
|
|
582
607
|
'.claude/commands/chati.md',
|
|
583
608
|
'.claude/rules/chati/',
|
|
@@ -822,9 +847,14 @@ export function copyCliDependencies(pkgDir, destNodeModules) {
|
|
|
822
847
|
let p = require.resolve(depName);
|
|
823
848
|
while (p && p !== dirname(p)) {
|
|
824
849
|
p = dirname(p);
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
850
|
+
const candidatePackage = join(p, 'package.json');
|
|
851
|
+
if (existsSync(candidatePackage)) {
|
|
852
|
+
try {
|
|
853
|
+
if (JSON.parse(readFileSync(candidatePackage, 'utf-8')).name === depName) {
|
|
854
|
+
depDir = p;
|
|
855
|
+
break;
|
|
856
|
+
}
|
|
857
|
+
} catch { /* keep walking */ }
|
|
828
858
|
}
|
|
829
859
|
}
|
|
830
860
|
} catch { continue; }
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Translate repository paths into their installed-project locations.
|
|
3
|
+
*
|
|
4
|
+
* Source files live under `chati.dev/`. Installed framework files live under
|
|
5
|
+
* `.chati.dev/`, while user-facing artifacts remain at project root.
|
|
6
|
+
*/
|
|
7
|
+
export function applyInstalledPathReplacement(content) {
|
|
8
|
+
return content
|
|
9
|
+
.replace(/chati\.dev\/artifacts\//g, 'artifacts/')
|
|
10
|
+
.replace(/(?<!\.)chati\.dev\//g, '.chati.dev/')
|
|
11
|
+
.replace(/(?<!\.)'chati\.dev'/g, "'.chati.dev'")
|
|
12
|
+
.replace(/(?<!\.)"chati\.dev"/g, '".chati.dev"');
|
|
13
|
+
}
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
14
14
|
import { join, dirname } from 'path';
|
|
15
15
|
import { adaptFrameworkFile, ADAPTABLE_FILES } from '../config/framework-adapter.js';
|
|
16
|
+
import { applyInstalledPathReplacement } from './path-replacement.js';
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
19
|
* Generate adapted framework overlays for all enabled providers.
|
|
@@ -41,7 +42,8 @@ export function generateProviderOverlays(targetDir, frameworkSource, allProvider
|
|
|
41
42
|
|
|
42
43
|
// Read CANONICAL (Claude) content from source, then adapt for target provider
|
|
43
44
|
const canonicalContent = readFileSync(srcPath, 'utf-8');
|
|
44
|
-
const
|
|
45
|
+
const installedContent = applyInstalledPathReplacement(canonicalContent);
|
|
46
|
+
const adapted = adaptFrameworkFile(installedContent, file, provider);
|
|
45
47
|
|
|
46
48
|
const destPath = join(overlayDir, file);
|
|
47
49
|
mkdirSync(dirname(destPath), { recursive: true });
|