chati-dev 4.5.7 → 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 +53 -48
- 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/package.json +1 -1
- package/src/config/claude-settings-generator.js +6 -6
- package/src/installer/core.js +51 -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
|
@@ -13,7 +13,7 @@ You are the **Dev Agent**, responsible for implementing code based on the approv
|
|
|
13
13
|
- **Duration**: Varies per task
|
|
14
14
|
- **Ratio**: 20% Human / 80% AI (interactive) or 5% Human / 95% AI (autonomous)
|
|
15
15
|
- **Absorbs**: Dev personas, self-validation patterns, Design System token enforcement, Ralph Wiggum autonomous mode, blocker taxonomy
|
|
16
|
-
- **
|
|
16
|
+
- **Execution binding**: resolved at runtime by installation-v2 task routing; never self-select or downgrade
|
|
17
17
|
- **Provider**: claude (default) | gemini (when large codebase tasks)
|
|
18
18
|
|
|
19
19
|
## Required MCPs
|
|
@@ -12,7 +12,7 @@ You are the **DevOps Agent**, responsible for shipping the project: Git operatio
|
|
|
12
12
|
- **Question Answered**: SHIP it
|
|
13
13
|
- **Duration**: 15-30 min
|
|
14
14
|
- **Ratio**: 30% Human / 70% AI
|
|
15
|
-
- **
|
|
15
|
+
- **Execution binding**: resolved at runtime by installation-v2 task routing; never self-select or downgrade
|
|
16
16
|
- **Provider**: claude (default)
|
|
17
17
|
- **Absorbs**: Docs-gen (auto documentation generation)
|
|
18
18
|
|
|
@@ -12,7 +12,7 @@ You are the **Brief Agent**, responsible for extracting and structuring the core
|
|
|
12
12
|
- **Question Answered**: WHAT is the problem?
|
|
13
13
|
- **Duration**: 30-60 min
|
|
14
14
|
- **Ratio**: 90% Human / 10% AI
|
|
15
|
-
- **
|
|
15
|
+
- **Execution binding**: resolved at runtime by installation-v2 task routing; never self-select or downgrade
|
|
16
16
|
- **Provider**: claude (default)
|
|
17
17
|
|
|
18
18
|
## Required MCPs
|
|
@@ -12,7 +12,7 @@ You are the **Brownfield WU Agent**, responsible for comprehensive discovery of
|
|
|
12
12
|
- **Question Answered**: HOW does it work today? (both code and operations)
|
|
13
13
|
- **Duration**: 1-4h depending on codebase size
|
|
14
14
|
- **Ratio**: 40% Human / 60% AI
|
|
15
|
-
- **
|
|
15
|
+
- **Execution binding**: resolved at runtime by installation-v2 task routing; never self-select or downgrade
|
|
16
16
|
- **Provider**: claude (default) | gemini (when codebase > 100K LOC)
|
|
17
17
|
|
|
18
18
|
## Required MCPs
|
|
@@ -12,7 +12,7 @@ You are the **Greenfield WU Agent**, responsible for understanding HOW the user
|
|
|
12
12
|
- **Question Answered**: HOW does it work today?
|
|
13
13
|
- **Duration**: 15-30 min
|
|
14
14
|
- **Ratio**: 70% Human / 30% AI
|
|
15
|
-
- **
|
|
15
|
+
- **Execution binding**: resolved at runtime by installation-v2 task routing; never self-select or downgrade
|
|
16
16
|
- **Provider**: claude (default)
|
|
17
17
|
|
|
18
18
|
## Required MCPs
|
|
@@ -9,7 +9,7 @@ You are the **Data Engineer**, a specialist sub-agent of the Architect Manager.
|
|
|
9
9
|
- **Role**: Database Architecture & Data Operations Specialist
|
|
10
10
|
- **Parent**: Architect Manager
|
|
11
11
|
- **Step**: 4 (Data Architecture - expanded from original Architect)
|
|
12
|
-
- **
|
|
12
|
+
- **Execution binding**: resolved at runtime by installation-v2 task routing; never self-select or downgrade
|
|
13
13
|
|
|
14
14
|
---
|
|
15
15
|
|
|
@@ -9,7 +9,7 @@ You are the **System Architect**, a specialist sub-agent of the Architect Manage
|
|
|
9
9
|
- **Role**: System Architecture & API Design Specialist
|
|
10
10
|
- **Parent**: Architect Manager
|
|
11
11
|
- **Steps**: 1-3 (Tech Stack, System Design, API + Auth + Deploy + Security)
|
|
12
|
-
- **
|
|
12
|
+
- **Execution binding**: resolved at runtime by installation-v2 task routing; never self-select or downgrade
|
|
13
13
|
|
|
14
14
|
---
|
|
15
15
|
|
|
@@ -12,7 +12,7 @@ You are the **Architect Manager**, the coordinator for the technical design pipe
|
|
|
12
12
|
- **Question Answered**: HOW will we build it?
|
|
13
13
|
- **Duration**: 45-90 min (coordinates 2 sub-agents sequentially)
|
|
14
14
|
- **Ratio**: 50% Human / 50% AI
|
|
15
|
-
- **
|
|
15
|
+
- **Execution binding**: resolved at runtime by installation-v2 task routing; never self-select or downgrade
|
|
16
16
|
- **Provider**: claude (default)
|
|
17
17
|
|
|
18
18
|
## Required MCPs
|
|
@@ -13,7 +13,7 @@ You are the **Detail Agent**, responsible for transforming the Brief into a form
|
|
|
13
13
|
- **Duration**: 45-90 min
|
|
14
14
|
- **Ratio**: 70% Human / 30% AI
|
|
15
15
|
- **Absorbs**: PM (PRD creation, product strategy), Analyst (market research, competitive analysis)
|
|
16
|
-
- **
|
|
16
|
+
- **Execution binding**: resolved at runtime by installation-v2 task routing; never self-select or downgrade
|
|
17
17
|
- **Provider**: claude (default)
|
|
18
18
|
|
|
19
19
|
## Required MCPs
|
|
@@ -13,7 +13,7 @@ You are the **Phases Agent**, responsible for breaking the PRD into development
|
|
|
13
13
|
- **Duration**: 20-40 min
|
|
14
14
|
- **Ratio**: 60% Human / 40% AI
|
|
15
15
|
- **Absorbs**: SM (phase planning, prioritization, wave sequencing)
|
|
16
|
-
- **
|
|
16
|
+
- **Execution binding**: resolved at runtime by installation-v2 task routing; never self-select or downgrade
|
|
17
17
|
- **Provider**: claude (default)
|
|
18
18
|
|
|
19
19
|
## Required MCPs
|
|
@@ -13,7 +13,7 @@ You are the **Tasks Agent**, responsible for breaking phases into atomic, execut
|
|
|
13
13
|
- **Duration**: 30-60 min
|
|
14
14
|
- **Ratio**: 50% Human / 50% AI
|
|
15
15
|
- **Absorbs**: SM (structured task creation, Given-When-Then criteria)
|
|
16
|
-
- **
|
|
16
|
+
- **Execution binding**: resolved at runtime by installation-v2 task routing; never self-select or downgrade
|
|
17
17
|
- **Provider**: claude (default)
|
|
18
18
|
|
|
19
19
|
## Required MCPs
|
|
@@ -9,7 +9,7 @@ You are the **Brand & Design System Architect**, a specialist sub-agent of the U
|
|
|
9
9
|
- **Role**: Brand Identity & Design System Specialist
|
|
10
10
|
- **Parent**: UX Manager
|
|
11
11
|
- **Phases**: 0 (Brand Identity) + 4 (Design System Definition)
|
|
12
|
-
- **
|
|
12
|
+
- **Execution binding**: resolved at runtime by installation-v2 task routing; never self-select or downgrade
|
|
13
13
|
|
|
14
14
|
---
|
|
15
15
|
|
|
@@ -9,7 +9,7 @@ You are the **Component Engineer**, a specialist sub-agent of the UX Manager. Yo
|
|
|
9
9
|
- **Role**: Component Architecture & Accessibility Specialist
|
|
10
10
|
- **Parent**: UX Manager
|
|
11
11
|
- **Phases**: 3 (Interaction Patterns + Accessibility + Motion + Component Discovery)
|
|
12
|
-
- **
|
|
12
|
+
- **Execution binding**: resolved at runtime by installation-v2 task routing; never self-select or downgrade
|
|
13
13
|
|
|
14
14
|
---
|
|
15
15
|
|
|
@@ -9,7 +9,7 @@ You are the **UX Researcher**, a specialist sub-agent of the UX Manager. You own
|
|
|
9
9
|
- **Role**: User Experience Research & Information Architecture Specialist
|
|
10
10
|
- **Parent**: UX Manager
|
|
11
11
|
- **Phases**: 1 (User Flow Mapping) + 2 (Information Architecture)
|
|
12
|
-
- **
|
|
12
|
+
- **Execution binding**: resolved at runtime by installation-v2 task routing; never self-select or downgrade
|
|
13
13
|
|
|
14
14
|
---
|
|
15
15
|
|
|
@@ -12,7 +12,7 @@ You are the **UX Manager**, the coordinator for the design experience pipeline.
|
|
|
12
12
|
- **Question Answered**: HOW will it look/feel?
|
|
13
13
|
- **Duration**: 60-120 min (coordinates 3 sub-agents sequentially)
|
|
14
14
|
- **Ratio**: 30% Human / 70% AI (user validates direction, sub-agents execute)
|
|
15
|
-
- **
|
|
15
|
+
- **Execution binding**: resolved at runtime by installation-v2 task routing; never self-select or downgrade
|
|
16
16
|
- **Provider**: claude (default)
|
|
17
17
|
|
|
18
18
|
## Required MCPs
|
|
@@ -12,7 +12,7 @@ You are the **QA-Implementation Agent**, the quality gate between BUILD and DEPL
|
|
|
12
12
|
- **Question Answered**: DOES it work correctly?
|
|
13
13
|
- **Duration**: 15-45 min (mostly automated)
|
|
14
14
|
- **Ratio**: 90% AI / 10% Human
|
|
15
|
-
- **
|
|
15
|
+
- **Execution binding**: resolved at runtime by installation-v2 task routing; never self-select or downgrade
|
|
16
16
|
- **Provider**: claude (default)
|
|
17
17
|
- **Absorbs**: Tests + SAST + CodeRabbit code review
|
|
18
18
|
|
|
@@ -12,7 +12,7 @@ You are the **QA-Planning Agent**, the quality gate between PLANNING (planning)
|
|
|
12
12
|
- **Question Answered**: IS everything traceable and rigorous?
|
|
13
13
|
- **Duration**: 15-30 min (automated validation)
|
|
14
14
|
- **Ratio**: 95% AI / 5% Human
|
|
15
|
-
- **
|
|
15
|
+
- **Execution binding**: resolved at runtime by installation-v2 task routing; never self-select or downgrade
|
|
16
16
|
- **Provider**: claude (default)
|
|
17
17
|
- **Absorbs**: Manager (cross-artifact validation), PO (quality gate validation, coherence checks)
|
|
18
18
|
|
|
@@ -19,7 +19,7 @@ You are the **QA-Visual Agent**, a specialized quality gate focused exclusively
|
|
|
19
19
|
- **Question Answered**: DOES it look right?
|
|
20
20
|
- **Duration**: 5-15 min (mostly automated via visual-qa.js)
|
|
21
21
|
- **Ratio**: 80% AI / 20% Human
|
|
22
|
-
- **
|
|
22
|
+
- **Execution binding**: resolved at runtime by installation-v2 task routing; never self-select or downgrade
|
|
23
23
|
- **Provider**: claude (default)
|
|
24
24
|
- **Absorbs**: Phase 5d + Phase 5e from QA-Implementation (visual review and motion quality)
|
|
25
25
|
|
|
@@ -13,7 +13,7 @@ You are the **Visualizer**, a specialized sub-agent that reads sequential screen
|
|
|
13
13
|
## Identity
|
|
14
14
|
|
|
15
15
|
- **Role**: Visual Frame Analyst (sub-agent, never runs standalone)
|
|
16
|
-
- **
|
|
16
|
+
- **Execution binding**: resolved at runtime by installation-v2 task routing; never self-select or downgrade
|
|
17
17
|
- **Spawned by**: Brief (reference analysis) or QA-Visual (build validation)
|
|
18
18
|
- **Output**: Text-only analysis returned to the parent agent
|
|
19
19
|
|
package/framework/config.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Chati.dev System Context
|
|
2
2
|
|
|
3
3
|
## Framework
|
|
4
|
-
- **Version**: 4.5.
|
|
4
|
+
- **Version**: 4.5.8
|
|
5
5
|
- **Agents**: Specialized discovery and planning agents feeding provider-neutral RAIL execution and a sealed release lane
|
|
6
6
|
- **Constitution**: 25 Articles + Preamble
|
|
7
7
|
- **Quality**: 5 pipeline gates + 3-tier verdicts + Fault Vector Protocol (Article XXII)
|
|
@@ -29,6 +29,19 @@ const CACHE_TTL_MS = 4 * 60 * 60 * 1000; // 4 hours
|
|
|
29
29
|
const GLOBAL_DIR = join(homedir(), '.chati-dev');
|
|
30
30
|
const LICENSE_PATH = join(GLOBAL_DIR, 'license.yaml');
|
|
31
31
|
|
|
32
|
+
async function hasVerifiedOpenInstallation(projectDir = process.cwd()) {
|
|
33
|
+
const artifactPath = join(projectDir, '.chati', 'v2', 'installation.json');
|
|
34
|
+
const doctorPath = join(projectDir, '.chati.dev', '_cli', 'installer-v2', 'index.js');
|
|
35
|
+
if (!existsSync(artifactPath) || !existsSync(doctorPath)) return false;
|
|
36
|
+
try {
|
|
37
|
+
const artifact = JSON.parse(readFileSync(artifactPath, 'utf-8'));
|
|
38
|
+
const { doctorV2 } = await import(doctorPath);
|
|
39
|
+
return doctorV2(artifact).passed && artifact.installation?.profile === 'open';
|
|
40
|
+
} catch {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
32
45
|
/**
|
|
33
46
|
* Pure license validation function — provider-agnostic.
|
|
34
47
|
* Returns a verdict object that hook wrappers can translate to their
|
|
@@ -38,6 +51,12 @@ const LICENSE_PATH = join(GLOBAL_DIR, 'license.yaml');
|
|
|
38
51
|
* @returns {Promise<{valid: boolean, reason?: string, status?: string}>}
|
|
39
52
|
*/
|
|
40
53
|
export async function checkLicense() {
|
|
54
|
+
// The public Open profile does not require a commercial license. Trust the
|
|
55
|
+
// profile only after the canonical v2 doctor validates the signed artifact.
|
|
56
|
+
if (await hasVerifiedOpenInstallation()) {
|
|
57
|
+
return { valid: true, status: 'NOT_REQUIRED', profile: 'open' };
|
|
58
|
+
}
|
|
59
|
+
|
|
41
60
|
// Read global license file — REQUIRED for operation
|
|
42
61
|
if (!existsSync(LICENSE_PATH)) {
|
|
43
62
|
return { valid: false, reason: 'No license found. Run: npx chati-dev activate --key=YOUR-KEY\nGet a license at https://chati.dev/pricing', status: 'MISSING' };
|
package/framework/manifest.json
CHANGED
|
@@ -1,103 +1,103 @@
|
|
|
1
1
|
{
|
|
2
|
-
"createdAt": "2026-08-
|
|
2
|
+
"createdAt": "2026-08-27T12:02:42.275Z",
|
|
3
3
|
"files": {
|
|
4
4
|
"agents/build/dev.md": {
|
|
5
|
-
"hash": "
|
|
6
|
-
"size":
|
|
5
|
+
"hash": "13c2b3855c1f815154a2f4020b99fd8ee71e701b41783f5f26696222720a1466",
|
|
6
|
+
"size": 64962,
|
|
7
7
|
"type": "md"
|
|
8
8
|
},
|
|
9
9
|
"agents/deploy/devops.md": {
|
|
10
|
-
"hash": "
|
|
11
|
-
"size":
|
|
10
|
+
"hash": "f751c5d95d690627c45d885d0a484c2019aacda4111cb9785b70ea1813e67f40",
|
|
11
|
+
"size": 18025,
|
|
12
12
|
"type": "md"
|
|
13
13
|
},
|
|
14
14
|
"agents/discover/brief.md": {
|
|
15
|
-
"hash": "
|
|
16
|
-
"size":
|
|
15
|
+
"hash": "f5980b1c622103879b8fec400ab4461ba9bbdc0b60f498ddcdcdc4c8504ee919",
|
|
16
|
+
"size": 26193,
|
|
17
17
|
"type": "md"
|
|
18
18
|
},
|
|
19
19
|
"agents/discover/brownfield-wu.md": {
|
|
20
|
-
"hash": "
|
|
21
|
-
"size":
|
|
20
|
+
"hash": "b89e20408bdb0c672d6799507b0f9679c76df7f87f183f27b325070174ac490f",
|
|
21
|
+
"size": 18120,
|
|
22
22
|
"type": "md"
|
|
23
23
|
},
|
|
24
24
|
"agents/discover/greenfield-wu.md": {
|
|
25
|
-
"hash": "
|
|
26
|
-
"size":
|
|
25
|
+
"hash": "046d29a39c85faaae7f67e820f8755d08b84d18ce880041208b6d9432e00eac8",
|
|
26
|
+
"size": 17214,
|
|
27
27
|
"type": "md"
|
|
28
28
|
},
|
|
29
29
|
"agents/plan/architect-data-engineer.md": {
|
|
30
|
-
"hash": "
|
|
30
|
+
"hash": "092e76bf23eda8634863f05e1557a29044ec7afbde4cce3b103db26363a2cbc5",
|
|
31
31
|
"size": 10435,
|
|
32
32
|
"type": "md"
|
|
33
33
|
},
|
|
34
34
|
"agents/plan/architect-system.md": {
|
|
35
|
-
"hash": "
|
|
36
|
-
"size":
|
|
35
|
+
"hash": "e05732c0a944be2c4d52cb3cd394a521c10c7a5d04569abb9f5f9111a1db1ceb",
|
|
36
|
+
"size": 8790,
|
|
37
37
|
"type": "md"
|
|
38
38
|
},
|
|
39
39
|
"agents/plan/architect.md": {
|
|
40
|
-
"hash": "
|
|
41
|
-
"size":
|
|
40
|
+
"hash": "d3d39eb5ac360b4945187dd6ad6b00bb096c09de2cf99232e881b91a8ae1c04a",
|
|
41
|
+
"size": 13289,
|
|
42
42
|
"type": "md"
|
|
43
43
|
},
|
|
44
44
|
"agents/plan/detail.md": {
|
|
45
|
-
"hash": "
|
|
46
|
-
"size":
|
|
45
|
+
"hash": "310903bd1e7edc79ccd30ee83ccdfdea1b18f745629f6e2892a001f777771f6a",
|
|
46
|
+
"size": 19291,
|
|
47
47
|
"type": "md"
|
|
48
48
|
},
|
|
49
49
|
"agents/plan/phases.md": {
|
|
50
|
-
"hash": "
|
|
51
|
-
"size":
|
|
50
|
+
"hash": "04f53642c0624da30cc74ec26298731480d016e959ed3538f40054ea5a95d8af",
|
|
51
|
+
"size": 15863,
|
|
52
52
|
"type": "md"
|
|
53
53
|
},
|
|
54
54
|
"agents/plan/tasks.md": {
|
|
55
|
-
"hash": "
|
|
56
|
-
"size":
|
|
55
|
+
"hash": "d6440ac7e7ecc9a24d89f39b1786e3cb218565a16b12ecba888e8899abe57b48",
|
|
56
|
+
"size": 28257,
|
|
57
57
|
"type": "md"
|
|
58
58
|
},
|
|
59
59
|
"agents/plan/ux-brand-architect.md": {
|
|
60
|
-
"hash": "
|
|
61
|
-
"size":
|
|
60
|
+
"hash": "7bb987949fb404115735c82085c906db7a6b7b1baa4e6672f442114fe6c554b7",
|
|
61
|
+
"size": 28402,
|
|
62
62
|
"type": "md"
|
|
63
63
|
},
|
|
64
64
|
"agents/plan/ux-component-engineer.md": {
|
|
65
|
-
"hash": "
|
|
66
|
-
"size":
|
|
65
|
+
"hash": "65c72804f9ee40f814493b0e4a3db6d9d12f8ce34f8a4425d46a4bd23d522aa2",
|
|
66
|
+
"size": 19702,
|
|
67
67
|
"type": "md"
|
|
68
68
|
},
|
|
69
69
|
"agents/plan/ux-researcher.md": {
|
|
70
|
-
"hash": "
|
|
71
|
-
"size":
|
|
70
|
+
"hash": "230555bfa77ced7196cf082ccc3f6de2b531b81063082c8d5af61d81ef37c608",
|
|
71
|
+
"size": 6573,
|
|
72
72
|
"type": "md"
|
|
73
73
|
},
|
|
74
74
|
"agents/plan/ux.md": {
|
|
75
|
-
"hash": "
|
|
76
|
-
"size":
|
|
75
|
+
"hash": "1b4303659b12a4cc687a7b14da52c51d4f72ab16b892cd2c074a818c96cacbe1",
|
|
76
|
+
"size": 15252,
|
|
77
77
|
"type": "md"
|
|
78
78
|
},
|
|
79
79
|
"agents/quality/qa-implementation.md": {
|
|
80
|
-
"hash": "
|
|
81
|
-
"size":
|
|
80
|
+
"hash": "33d48d86473bd23b137baefb62ccfb5ad3f5a3e515779e2a19acc54ebe1de62b",
|
|
81
|
+
"size": 40366,
|
|
82
82
|
"type": "md"
|
|
83
83
|
},
|
|
84
84
|
"agents/quality/qa-planning.md": {
|
|
85
|
-
"hash": "
|
|
86
|
-
"size":
|
|
85
|
+
"hash": "82cf821a4a04b784788f3b53281e7aa49abd4e72a88d9fcd42f2cc5c5d9a0f6d",
|
|
86
|
+
"size": 25798,
|
|
87
87
|
"type": "md"
|
|
88
88
|
},
|
|
89
89
|
"agents/quality/qa-visual.md": {
|
|
90
|
-
"hash": "
|
|
91
|
-
"size":
|
|
90
|
+
"hash": "c4b7ca7ec0378c6a51092adfcaf58f811796711f713b65ac12408529374a72e9",
|
|
91
|
+
"size": 21520,
|
|
92
92
|
"type": "md"
|
|
93
93
|
},
|
|
94
94
|
"agents/shared/visualizer.md": {
|
|
95
|
-
"hash": "
|
|
96
|
-
"size":
|
|
95
|
+
"hash": "4110dcb1c2e440f31d9820499c7e946b58ac7fe2b9e7d29f68133c3c8f2021e9",
|
|
96
|
+
"size": 4375,
|
|
97
97
|
"type": "md"
|
|
98
98
|
},
|
|
99
99
|
"config.yaml": {
|
|
100
|
-
"hash": "
|
|
100
|
+
"hash": "00aa2be05f8cd997b3aae234f56969e1b3309af68c35ada1c439e66cefb37ae5",
|
|
101
101
|
"size": 2669,
|
|
102
102
|
"type": "yaml"
|
|
103
103
|
},
|
|
@@ -122,12 +122,12 @@
|
|
|
122
122
|
"type": "md"
|
|
123
123
|
},
|
|
124
124
|
"context/root.md": {
|
|
125
|
-
"hash": "
|
|
125
|
+
"hash": "f04a8dc22486c1f0cc29ea2ec5ad30d4a1c343823f1dfc5674946acb854106bb",
|
|
126
126
|
"size": 1217,
|
|
127
127
|
"type": "md"
|
|
128
128
|
},
|
|
129
129
|
"data/entity-registry.yaml": {
|
|
130
|
-
"hash": "
|
|
130
|
+
"hash": "d11e07cb0ab2790938af077da954580f9bf0d7230bf4e79b59e6c45a00e2dcb7",
|
|
131
131
|
"size": 34605,
|
|
132
132
|
"type": "yaml"
|
|
133
133
|
},
|
|
@@ -307,8 +307,8 @@
|
|
|
307
307
|
"type": "js"
|
|
308
308
|
},
|
|
309
309
|
"hooks/license-guard.js": {
|
|
310
|
-
"hash": "
|
|
311
|
-
"size":
|
|
310
|
+
"hash": "40b0d044e2dac77fca826825e52119b1f2293a3b63225d1aecc9116ed7dc695d",
|
|
311
|
+
"size": 11759,
|
|
312
312
|
"type": "js"
|
|
313
313
|
},
|
|
314
314
|
"hooks/mode-governance.js": {
|
|
@@ -462,8 +462,8 @@
|
|
|
462
462
|
"type": "yaml"
|
|
463
463
|
},
|
|
464
464
|
"orchestrator/chati-router.js": {
|
|
465
|
-
"hash": "
|
|
466
|
-
"size":
|
|
465
|
+
"hash": "f5c3e7b8285d32cbb1e1f9e0bfda23f8996a4bb3e1b3966038b41d06aacc50d8",
|
|
466
|
+
"size": 20793,
|
|
467
467
|
"type": "js"
|
|
468
468
|
},
|
|
469
469
|
"orchestrator/chati-update.md": {
|
|
@@ -472,10 +472,15 @@
|
|
|
472
472
|
"type": "md"
|
|
473
473
|
},
|
|
474
474
|
"orchestrator/chati.md": {
|
|
475
|
-
"hash": "
|
|
476
|
-
"size":
|
|
475
|
+
"hash": "8d55e3e354e7e7cf4d28ca3aba5372df0580ca3b191e2e14696363867eba567a",
|
|
476
|
+
"size": 45355,
|
|
477
477
|
"type": "md"
|
|
478
478
|
},
|
|
479
|
+
"package.json": {
|
|
480
|
+
"hash": "3ca9d4afd21425087cf31893b8f9f63c81b0b8408db5e343ca76e5f8aa26ab9a",
|
|
481
|
+
"size": 23,
|
|
482
|
+
"type": "json"
|
|
483
|
+
},
|
|
479
484
|
"patterns/elicitation-library.yaml": {
|
|
480
485
|
"hash": "223780498a36981fa030ae54f2e73bbc68140073b7c30a286465cffe45886f17",
|
|
481
486
|
"size": 25578,
|
|
@@ -1347,5 +1352,5 @@
|
|
|
1347
1352
|
"type": "yaml"
|
|
1348
1353
|
}
|
|
1349
1354
|
},
|
|
1350
|
-
"version": "4.5.
|
|
1355
|
+
"version": "4.5.8"
|
|
1351
1356
|
}
|
package/framework/manifest.sig
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
stchaq/GjnloDsRgwm8vCQpA4vFKsofjuyMg5b+4aU6pEblso6UgEAtPPlRkVVkxcmn8ctX9YxQmqe+MmaPsAA==
|
|
@@ -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.
|
package/package.json
CHANGED
|
@@ -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.)
|
|
@@ -826,9 +847,14 @@ export function copyCliDependencies(pkgDir, destNodeModules) {
|
|
|
826
847
|
let p = require.resolve(depName);
|
|
827
848
|
while (p && p !== dirname(p)) {
|
|
828
849
|
p = dirname(p);
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
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 */ }
|
|
832
858
|
}
|
|
833
859
|
}
|
|
834
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 });
|
|
@@ -35,6 +35,54 @@ function assertString(value, code, label) {
|
|
|
35
35
|
if (typeof value !== 'string' || value.trim() === '') throw new ContractError(code, `${label} must be a non-empty string`);
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
function assertExactKeys(value, allowedKeys, label) {
|
|
39
|
+
assertObject(value, 'INVALID_ARTIFACT', label);
|
|
40
|
+
const unexpected = Object.keys(value).filter((key) => !allowedKeys.has(key));
|
|
41
|
+
if (unexpected.length > 0) {
|
|
42
|
+
throw new ContractError('UNEXPECTED_ARTIFACT_FIELD', `${label} contains unsupported fields: ${unexpected.join(', ')}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const ARTIFACT_KEYS = new Set([
|
|
47
|
+
'schema_version', 'artifact_type', 'project_id', 'created_at', 'installation',
|
|
48
|
+
'capability_snapshot', 'brain_read_capability', 'managed_paths',
|
|
49
|
+
'installation_artifact_id', 'digest',
|
|
50
|
+
]);
|
|
51
|
+
const INSTALLATION_KEYS = new Set([
|
|
52
|
+
'installation_id', 'profile', 'enabled_providers', 'capability_snapshot_ref',
|
|
53
|
+
'policy_ref', 'primary_binding', 'primary_harness', 'external_integrations',
|
|
54
|
+
]);
|
|
55
|
+
const BINDING_KEYS = new Set([
|
|
56
|
+
'provider_id', 'harness_id', 'allowed_models', 'allowed_reasoning_configurations',
|
|
57
|
+
]);
|
|
58
|
+
const SNAPSHOT_KEYS = new Set(['snapshot_id', 'expires_at', 'models']);
|
|
59
|
+
const MODEL_KEYS = new Set([
|
|
60
|
+
'provider_id', 'model_id', 'actions', 'adjudication_priority',
|
|
61
|
+
'highest_reasoning_configuration', 'routing_priority', 'tier', 'natural_role',
|
|
62
|
+
]);
|
|
63
|
+
|
|
64
|
+
function assertClosedArtifactSchema(artifact) {
|
|
65
|
+
assertExactKeys(artifact, ARTIFACT_KEYS, 'artifact');
|
|
66
|
+
assertExactKeys(artifact.installation, INSTALLATION_KEYS, 'installation');
|
|
67
|
+
for (const binding of artifact.installation.enabled_providers ?? []) {
|
|
68
|
+
assertExactKeys(binding, BINDING_KEYS, 'enabled provider binding');
|
|
69
|
+
}
|
|
70
|
+
if (artifact.installation.primary_binding !== undefined) {
|
|
71
|
+
assertExactKeys(artifact.installation.primary_binding, new Set(['provider_id', 'harness_id']), 'primary_binding');
|
|
72
|
+
}
|
|
73
|
+
if (artifact.installation.external_integrations !== undefined) {
|
|
74
|
+
assertExactKeys(artifact.installation.external_integrations, new Set(['clickup']), 'external_integrations');
|
|
75
|
+
}
|
|
76
|
+
assertExactKeys(artifact.capability_snapshot, SNAPSHOT_KEYS, 'capability_snapshot');
|
|
77
|
+
for (const model of artifact.capability_snapshot.models ?? []) {
|
|
78
|
+
assertExactKeys(model, MODEL_KEYS, 'capability model');
|
|
79
|
+
}
|
|
80
|
+
const brainKeys = artifact.brain_read_capability?.status === 'configured'
|
|
81
|
+
? new Set(['status', 'mode', 'policy_ref'])
|
|
82
|
+
: new Set(['status']);
|
|
83
|
+
assertExactKeys(artifact.brain_read_capability, brainKeys, 'brain_read_capability');
|
|
84
|
+
}
|
|
85
|
+
|
|
38
86
|
function readClock(clock) {
|
|
39
87
|
if (typeof clock !== 'function') throw new ContractError('MISSING_CLOCK', 'a deterministic clock function is required');
|
|
40
88
|
const result = clock();
|
|
@@ -127,6 +175,7 @@ export function doctorV2(artifact, { clock = () => new Date() } = {}) {
|
|
|
127
175
|
const checks = [];
|
|
128
176
|
try {
|
|
129
177
|
assertObject(artifact, 'INVALID_ARTIFACT', 'artifact');
|
|
178
|
+
assertClosedArtifactSchema(artifact);
|
|
130
179
|
if (artifact.schema_version !== INSTALLATION_SCHEMA_VERSION || artifact.artifact_type !== 'chati-installation') {
|
|
131
180
|
throw new ContractError('UNSUPPORTED_INSTALLATION_ARTIFACT', 'expected a chati installation artifact v2');
|
|
132
181
|
}
|
|
@@ -138,6 +187,12 @@ export function doctorV2(artifact, { clock = () => new Date() } = {}) {
|
|
|
138
187
|
if (!Array.isArray(artifact.managed_paths) || !artifact.managed_paths.every((path) => ensureRelativePath(path))) {
|
|
139
188
|
throw new ContractError('INVALID_MANAGED_PATH', 'managed_paths must be valid project-relative paths');
|
|
140
189
|
}
|
|
190
|
+
if (!artifact.managed_paths.includes(INSTALLATION_ARTIFACT_PATH)) {
|
|
191
|
+
throw new ContractError('INVALID_MANAGED_PATH', `managed_paths must include ${INSTALLATION_ARTIFACT_PATH}`);
|
|
192
|
+
}
|
|
193
|
+
if (new Set(artifact.managed_paths).size !== artifact.managed_paths.length) {
|
|
194
|
+
throw new ContractError('DUPLICATE_MANAGED_PATH', 'managed_paths must be unique');
|
|
195
|
+
}
|
|
141
196
|
checks.push({ name: 'artifact', status: 'pass', message: 'artifact schema and digest are valid' });
|
|
142
197
|
checks.push({ name: 'selected-bindings', status: 'pass', message: 'only explicitly selected provider/harness bindings are present' });
|
|
143
198
|
checks.push({ name: 'brain-capability', status: 'pass', message: 'Brain capability is absent or read-only' });
|