chati-dev 4.0.11 → 4.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -9
- package/bin/chati.js +17 -1
- package/framework/agents/build/dev.md +43 -10
- package/framework/agents/discover/brief.md +38 -8
- package/framework/agents/discover/brownfield-wu.md +44 -3
- package/framework/agents/discover/greenfield-wu.md +14 -0
- package/framework/agents/plan/architect-data-engineer.md +278 -0
- package/framework/agents/plan/architect-system.md +174 -0
- package/framework/agents/plan/architect.md +97 -301
- package/framework/agents/plan/detail.md +25 -0
- package/framework/agents/plan/tasks.md +29 -1
- package/framework/agents/plan/ux-brand-architect.md +215 -0
- package/framework/agents/plan/ux-component-engineer.md +289 -0
- package/framework/agents/plan/ux-researcher.md +166 -0
- package/framework/agents/plan/ux.md +126 -1008
- package/framework/agents/quality/qa-implementation.md +121 -22
- package/framework/agents/quality/qa-planning.md +18 -0
- package/framework/config.yaml +15 -4
- package/framework/constitution.md +9 -3
- package/framework/context/root.md +2 -2
- package/framework/data/entity-registry.yaml +59 -3
- package/framework/hooks/constitution-guard.js +67 -1
- package/framework/hooks/license-guard.js +4 -4
- package/framework/hooks/model-governance.js +2 -1
- package/framework/hooks/prism-engine.js +74 -6
- package/framework/hooks/read-protection.js +1 -1
- package/framework/hooks/session-digest.js +159 -7
- package/framework/hooks/settings.json +16 -4
- package/framework/hooks/style-guard.js +134 -0
- package/framework/hooks/undercover-guard.js +220 -0
- package/framework/i18n/en.yaml +2 -2
- package/framework/i18n/es.yaml +2 -2
- package/framework/i18n/fr.yaml +2 -2
- package/framework/i18n/pt.yaml +2 -2
- package/framework/intelligence/context-engine.md +21 -0
- package/framework/intelligence/hooks-performance.md +54 -0
- package/framework/intelligence/memory-layer.md +47 -0
- package/framework/migrations/v4.0-to-v4.1.yaml +165 -0
- package/framework/orchestrator/chati.md +327 -1067
- package/framework/schemas/session.schema.json +1 -1
- package/framework/templates/brandbook-html-tmpl.md +107 -0
- package/framework/templates/session-memory-tmpl.yaml +68 -0
- package/package.json +1 -1
- package/scripts/doctor/checks/agents.js +1 -1
- package/scripts/health-check.js +1 -1
- package/src/autonomy/build-state.js +1 -1
- package/src/autonomy/worktree-manager.js +13 -13
- package/src/config/agent-customizer.js +1 -1
- package/src/config/gemini-hooks-generator.js +149 -6
- package/src/config/ide-configs.js +1 -1
- package/src/context/bracket-tracker.js +25 -4
- package/src/context/engine.js +37 -7
- package/src/context/formatter.js +45 -1
- package/src/context/index.js +3 -3
- package/src/dashboard/data-reader.js +7 -7
- package/src/decision/engine.js +2 -2
- package/src/decision/registry-healer.js +1 -1
- package/src/decision/registry-updater.js +2 -2
- package/src/extensions/loader.js +1 -1
- package/src/gates/g1-planning-complete.js +1 -1
- package/src/gates/g2-qa-planning.js +1 -1
- package/src/gates/g3-implementation.js +4 -4
- package/src/gates/g4-qa-implementation.js +2 -2
- package/src/health/auto-fix.js +3 -3
- package/src/health/engine.js +2 -2
- package/src/installer/brownfield-upgrader.js +3 -3
- package/src/installer/manifest.js +1 -1
- package/src/installer/transaction.js +1 -1
- package/src/installer/validator.js +4 -4
- package/src/intelligence/decision-engine.js +1 -1
- package/src/intelligence/memory-manager.js +1 -1
- package/src/intelligence/registry-manager.js +2 -2
- package/src/intelligence/timeline.js +1 -1
- package/src/license/client.js +1 -1
- package/src/license/commands.js +2 -2
- package/src/memory/agent-memory.js +3 -3
- package/src/memory/daily-digest.js +170 -0
- package/src/memory/dream.js +254 -0
- package/src/memory/gotchas.js +2 -2
- package/src/memory/index.js +18 -0
- package/src/memory/magic-docs.js +98 -0
- package/src/memory/memory-extractor.js +163 -0
- package/src/memory/session-digest.js +144 -6
- package/src/merger/yaml-merger.js +1 -1
- package/src/orchestrator/cli.js +980 -0
- package/src/orchestrator/handoff-engine.js +31 -4
- package/src/orchestrator/index.js +9 -0
- package/src/orchestrator/session-manager.js +1 -1
- package/src/preview/detector.js +3 -3
- package/src/preview/launcher.js +2 -2
- package/src/quality/metrics-collector.js +1 -1
- package/src/quality/test-runner.js +2 -4
- package/src/scanning/density-scanner.js +51 -0
- package/src/scanning/env-scanner.js +97 -0
- package/src/scanning/index.js +7 -0
- package/src/scanning/leakage-scanner.js +54 -0
- package/src/scanning/placeholder-scanner.js +40 -0
- package/src/scanning/security-scanner.js +94 -0
- package/src/security/bash-security.js +335 -0
- package/src/security/index.js +9 -0
- package/src/telemetry/config.js +3 -3
- package/src/telemetry/sender.js +1 -1
- package/src/terminal/cli-registry.js +1 -1
- package/src/terminal/cost-tracker.js +1 -2
- package/src/terminal/prompt-builder.js +11 -27
- package/src/terminal/rate-limiter.js +1 -2
- package/src/terminal/run-agent.js +3 -3
- package/src/terminal/spawner.js +41 -4
- package/src/upgrade/checker.js +2 -2
- package/src/upgrade/migrator.js +1 -1
- package/src/utils/config-parser.js +1 -1
- package/src/utils/file-lock.js +3 -3
- package/src/utils/provider-limits.js +35 -4
- package/src/wizard/i18n.js +4 -4
- package/src/wizard/index.js +2 -1
- package/src/wizard/questions.js +4 -2
|
@@ -6,9 +6,13 @@
|
|
|
6
6
|
* recovered after context compaction. Writes to .chati/memories/shared/session/.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
9
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync } from 'fs';
|
|
10
10
|
import { join } from 'path';
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Extract structured 8-section digest from session state.
|
|
14
|
+
* Enriched version captures decisions, blockers, gotchas for better recovery.
|
|
15
|
+
*/
|
|
12
16
|
function extractDigest(projectDir) {
|
|
13
17
|
const sessionPath = join(projectDir, '.chati', 'session.yaml');
|
|
14
18
|
if (!existsSync(sessionPath)) return null;
|
|
@@ -19,13 +23,40 @@ function extractDigest(projectDir) {
|
|
|
19
23
|
return match ? match[1].trim().replace(/^["']|["']$/g, '') : null;
|
|
20
24
|
};
|
|
21
25
|
|
|
22
|
-
|
|
26
|
+
// Base digest (backward-compatible)
|
|
27
|
+
const base = {
|
|
23
28
|
timestamp: new Date().toISOString(),
|
|
24
29
|
mode: extract('mode') || 'discover',
|
|
25
30
|
currentAgent: extract('current_agent') || 'none',
|
|
26
31
|
pipelinePosition: extract('pipeline_position') || 'unknown',
|
|
27
32
|
workflow: extract('workflow') || 'unknown',
|
|
28
33
|
};
|
|
34
|
+
|
|
35
|
+
// Enrich with gotchas (lightweight read, capped at 10)
|
|
36
|
+
let gotchas = [];
|
|
37
|
+
try {
|
|
38
|
+
const gotchasPath = join(projectDir, '.chati', 'memories', 'shared', 'gotchas.json');
|
|
39
|
+
if (existsSync(gotchasPath)) {
|
|
40
|
+
const gotchasData = JSON.parse(readFileSync(gotchasPath, 'utf-8'));
|
|
41
|
+
const entries = Array.isArray(gotchasData) ? gotchasData : (gotchasData.gotchas || []);
|
|
42
|
+
gotchas = entries.slice(0, 10).map(g => ({
|
|
43
|
+
id: g.id,
|
|
44
|
+
message: (g.message || '').slice(0, 200),
|
|
45
|
+
severity: g.severity,
|
|
46
|
+
}));
|
|
47
|
+
}
|
|
48
|
+
} catch { /* ignore read errors */ }
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
...base,
|
|
52
|
+
version: '2.0',
|
|
53
|
+
// Structured sections (enriched)
|
|
54
|
+
active_task: extract('active_task') || null,
|
|
55
|
+
blockers: [], // Populated by agents at runtime
|
|
56
|
+
decisions_made: [], // Populated by agents at runtime
|
|
57
|
+
gotchas_found: gotchas,
|
|
58
|
+
next_steps: [], // Populated by agents at runtime
|
|
59
|
+
};
|
|
29
60
|
}
|
|
30
61
|
|
|
31
62
|
async function main() {
|
|
@@ -44,15 +75,136 @@ async function main() {
|
|
|
44
75
|
mkdirSync(digestDir, { recursive: true });
|
|
45
76
|
|
|
46
77
|
const fileName = `digest-${Date.now()}.yaml`;
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
78
|
+
// Serialize to YAML-like format supporting arrays and objects
|
|
79
|
+
const lines = [];
|
|
80
|
+
for (const [k, v] of Object.entries(digest)) {
|
|
81
|
+
if (Array.isArray(v)) {
|
|
82
|
+
if (v.length === 0) {
|
|
83
|
+
lines.push(`${k}: []`);
|
|
84
|
+
} else {
|
|
85
|
+
lines.push(`${k}:`);
|
|
86
|
+
for (const item of v) {
|
|
87
|
+
lines.push(` - ${typeof item === 'object' ? JSON.stringify(item) : item}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
} else if (v === null) {
|
|
91
|
+
lines.push(`${k}: null`);
|
|
92
|
+
} else {
|
|
93
|
+
lines.push(`${k}: ${JSON.stringify(String(v))}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
writeFileSync(join(digestDir, fileName), lines.join('\n') + '\n', 'utf-8');
|
|
98
|
+
|
|
99
|
+
// Auto daily digest: append session entry on every PreCompact
|
|
100
|
+
try {
|
|
101
|
+
const dailyDir = join(projectDir, '.chati', 'memories', 'shared', 'daily');
|
|
102
|
+
mkdirSync(dailyDir, { recursive: true });
|
|
103
|
+
const dateStr = new Date().toISOString().split('T')[0];
|
|
104
|
+
const dailyPath = join(dailyDir, `${dateStr}.md`);
|
|
105
|
+
const time = new Date().toISOString().split('T')[1].split('.')[0];
|
|
106
|
+
const entry = `### ${time} -- ${digest.mode} (${digest.currentAgent})\n- Pipeline: ${digest.pipelinePosition}\n\n`;
|
|
107
|
+
let existing = '';
|
|
108
|
+
if (existsSync(dailyPath)) {
|
|
109
|
+
existing = readFileSync(dailyPath, 'utf-8');
|
|
110
|
+
} else {
|
|
111
|
+
existing = `# Daily Digest -- ${dateStr}\n\n`;
|
|
112
|
+
}
|
|
113
|
+
writeFileSync(dailyPath, existing + entry, 'utf-8');
|
|
114
|
+
} catch (err) {
|
|
115
|
+
process.stderr.write(`[chati] session-digest daily-append: ${err.message}\n`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Auto memory consolidation: trigger when memories accumulate
|
|
119
|
+
try {
|
|
120
|
+
const memBase = join(projectDir, '.chati', 'memories');
|
|
121
|
+
if (existsSync(memBase)) {
|
|
122
|
+
let totalEntries = 0;
|
|
123
|
+
const dirs = readdirSync(memBase, { withFileTypes: true })
|
|
124
|
+
.filter(d => d.isDirectory() && d.name !== 'shared');
|
|
125
|
+
for (const d of dirs) {
|
|
126
|
+
const memFile = join(memBase, d.name, 'MEMORY.md');
|
|
127
|
+
if (existsSync(memFile)) {
|
|
128
|
+
const content = readFileSync(memFile, 'utf-8');
|
|
129
|
+
totalEntries += (content.match(/^- /gm) || []).length;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
// Consolidate when > 100 entries (50% of 200 cap)
|
|
133
|
+
if (totalEntries > 100) {
|
|
134
|
+
process.stderr.write(`[chati] auto-dream: ${totalEntries} entries, triggering consolidation\n`);
|
|
135
|
+
// Fire-and-forget: consolidation runs in background
|
|
136
|
+
// Import would fail in hook context, so we spawn a child process
|
|
137
|
+
const { execSync } = await import('child_process');
|
|
138
|
+
try {
|
|
139
|
+
execSync(`node -e "import('./packages/chati-dev/src/memory/dream.js').then(m => m.runDreamConsolidation('${projectDir.replace(/'/g, "\\'")}'))"`, {
|
|
140
|
+
cwd: projectDir,
|
|
141
|
+
timeout: 10000,
|
|
142
|
+
stdio: 'ignore',
|
|
143
|
+
});
|
|
144
|
+
} catch { /* expected: consolidation may timeout or fail */ }
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
} catch (err) {
|
|
148
|
+
process.stderr.write(`[chati] session-digest auto-dream: ${err.message}\n`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Session telemetry ping (moved from license-guard to reduce per-prompt overhead)
|
|
153
|
+
// 5-minute throttle with phase/agent change detection
|
|
154
|
+
try {
|
|
155
|
+
const homedir = process.env.HOME || '';
|
|
156
|
+
const pingsPath = join(homedir, '.chati-dev', 'pings.yaml');
|
|
157
|
+
const licensePath = join(homedir, '.chati-dev', 'license.yaml');
|
|
158
|
+
|
|
159
|
+
if (existsSync(licensePath)) {
|
|
160
|
+
const licenseRaw = readFileSync(licensePath, 'utf-8');
|
|
161
|
+
const keyMatch = licenseRaw.match(/key:\s*["']?([^"'\n]+)/);
|
|
162
|
+
const licenseKey = keyMatch ? keyMatch[1].trim() : null;
|
|
163
|
+
|
|
164
|
+
if (licenseKey && digest) {
|
|
165
|
+
let shouldPing = false;
|
|
166
|
+
const now = Date.now();
|
|
167
|
+
const currentState = `${digest.mode}|${digest.currentAgent}`;
|
|
168
|
+
|
|
169
|
+
if (existsSync(pingsPath)) {
|
|
170
|
+
const pingsRaw = readFileSync(pingsPath, 'utf-8');
|
|
171
|
+
const entryMatch = pingsRaw.match(new RegExp(`${projectDir.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}.*?'([^']+)'`));
|
|
172
|
+
if (entryMatch) {
|
|
173
|
+
const [ts, phase, agent] = entryMatch[1].split('|');
|
|
174
|
+
const elapsed = now - parseInt(ts);
|
|
175
|
+
const stateChanged = `${phase}|${agent}` !== currentState;
|
|
176
|
+
shouldPing = elapsed > 300000 || stateChanged; // 5 min or state change
|
|
177
|
+
} else {
|
|
178
|
+
shouldPing = true;
|
|
179
|
+
}
|
|
180
|
+
} else {
|
|
181
|
+
shouldPing = true;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (shouldPing) {
|
|
185
|
+
// Update ping timestamp
|
|
186
|
+
mkdirSync(join(homedir, '.chati-dev'), { recursive: true });
|
|
187
|
+
writeFileSync(pingsPath, `'${projectDir}': '${now}|${currentState}'\n`, 'utf-8');
|
|
50
188
|
|
|
51
|
-
|
|
189
|
+
// Fire-and-forget telemetry (3s timeout, never blocks)
|
|
190
|
+
try {
|
|
191
|
+
const body = JSON.stringify({
|
|
192
|
+
license_key: licenseKey,
|
|
193
|
+
project_name: digest.currentAgent || 'unknown',
|
|
194
|
+
events: [{ type: 'session_active', timestamp: new Date().toISOString(), properties: { pipeline_phase: digest.mode, current_agent: digest.currentAgent } }],
|
|
195
|
+
});
|
|
196
|
+
fetch('https://chati.dev/api/telemetry', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body, signal: AbortSignal.timeout(3000) }).catch(() => {});
|
|
197
|
+
} catch { /* expected: network may be unavailable */ }
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
} catch (err) {
|
|
202
|
+
process.stderr.write(`[chati] session-digest telemetry-ping: ${err.message}\n`);
|
|
52
203
|
}
|
|
53
204
|
|
|
54
205
|
process.stdout.write(JSON.stringify({ result: 'allow' }));
|
|
55
|
-
} catch {
|
|
206
|
+
} catch (err) {
|
|
207
|
+
process.stderr.write(`[chati] session-digest: ${err.message}\n`);
|
|
56
208
|
process.stdout.write(JSON.stringify({ result: 'allow' }));
|
|
57
209
|
}
|
|
58
210
|
}
|
|
@@ -5,10 +5,6 @@
|
|
|
5
5
|
{
|
|
6
6
|
"matcher": ".*",
|
|
7
7
|
"hooks": [
|
|
8
|
-
{
|
|
9
|
-
"type": "command",
|
|
10
|
-
"command": "node chati.dev/hooks/license-guard.js"
|
|
11
|
-
},
|
|
12
8
|
{
|
|
13
9
|
"type": "command",
|
|
14
10
|
"command": "node chati.dev/hooks/prism-engine.js"
|
|
@@ -51,6 +47,14 @@
|
|
|
51
47
|
{
|
|
52
48
|
"type": "command",
|
|
53
49
|
"command": "node chati.dev/hooks/constitution-guard.js"
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"type": "command",
|
|
53
|
+
"command": "node chati.dev/hooks/undercover-guard.js"
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"type": "command",
|
|
57
|
+
"command": "node chati.dev/hooks/style-guard.js"
|
|
54
58
|
}
|
|
55
59
|
]
|
|
56
60
|
},
|
|
@@ -60,6 +64,14 @@
|
|
|
60
64
|
{
|
|
61
65
|
"type": "command",
|
|
62
66
|
"command": "node chati.dev/hooks/constitution-guard.js"
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"type": "command",
|
|
70
|
+
"command": "node chati.dev/hooks/undercover-guard.js"
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"type": "command",
|
|
74
|
+
"command": "node chati.dev/hooks/style-guard.js"
|
|
63
75
|
}
|
|
64
76
|
]
|
|
65
77
|
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Style Guard Hook — PreToolUse (Write/Edit/Bash)
|
|
4
|
+
*
|
|
5
|
+
* Enforces Constitution Article V writing standards:
|
|
6
|
+
* - No em-dashes (U+2013 EN DASH, U+2014 EM DASH)
|
|
7
|
+
* - No emojis in generated content
|
|
8
|
+
*
|
|
9
|
+
* Mode: Advisory (warn, not block). Agent receives warning and should self-correct.
|
|
10
|
+
* Feature-flag gated: respects anti_dash and no_emojis in config.yaml.
|
|
11
|
+
*
|
|
12
|
+
* Exempt paths: framework files (chati.dev/, .chati/), YAML configs, CLAUDE.md
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { existsSync, readFileSync } from 'fs';
|
|
16
|
+
import { join, basename } from 'path';
|
|
17
|
+
|
|
18
|
+
/** Em-dash and en-dash characters */
|
|
19
|
+
const EM_DASH_PATTERN = /[\u2013\u2014]/;
|
|
20
|
+
|
|
21
|
+
/** Emoji unicode ranges (comprehensive) */
|
|
22
|
+
const EMOJI_PATTERN = /[\u{1F600}-\u{1F64F}\u{1F300}-\u{1F5FF}\u{1F680}-\u{1F6FF}\u{1F1E0}-\u{1F1FF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}\u{1F900}-\u{1F9FF}\u{1FA00}-\u{1FA6F}\u{1FA70}-\u{1FAFF}\u{231A}-\u{231B}\u{23E9}-\u{23F3}\u{23F8}-\u{23FA}\u{25AA}-\u{25AB}\u{25B6}\u{25C0}\u{25FB}-\u{25FE}\u{2614}-\u{2615}\u{2648}-\u{2653}\u{267F}\u{2693}\u{26A1}\u{26AA}-\u{26AB}\u{26BD}-\u{26BE}\u{26C4}-\u{26C5}\u{26CE}\u{26D4}\u{26EA}\u{26F2}-\u{26F3}\u{26F5}\u{26FA}\u{26FD}\u{2702}\u{2705}\u{2708}-\u{270D}\u{270F}]/u;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Paths exempt from style enforcement.
|
|
26
|
+
* Framework files, configs, and context files may legitimately contain special chars.
|
|
27
|
+
*/
|
|
28
|
+
const EXEMPT_PATTERNS = [
|
|
29
|
+
/^chati\.dev\//,
|
|
30
|
+
/^\.chati\//,
|
|
31
|
+
/^\.claude\//,
|
|
32
|
+
/CLAUDE\.md$/,
|
|
33
|
+
/CLAUDE\.local\.md$/,
|
|
34
|
+
/GEMINI\.md$/,
|
|
35
|
+
/AGENTS\.md$/,
|
|
36
|
+
/\.yaml$/,
|
|
37
|
+
/\.yml$/,
|
|
38
|
+
/\.json$/,
|
|
39
|
+
/package\.json$/,
|
|
40
|
+
/\.lock$/,
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
function isExemptPath(filePath, projectDir) {
|
|
44
|
+
if (!filePath) return true;
|
|
45
|
+
const rel = filePath.startsWith(projectDir)
|
|
46
|
+
? filePath.slice(projectDir.length).replace(/^[/\\]/, '')
|
|
47
|
+
: filePath;
|
|
48
|
+
return EXEMPT_PATTERNS.some(p => p.test(rel));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Check content for style violations.
|
|
53
|
+
* @returns {{ violations: string[], hasDash: boolean, hasEmoji: boolean }}
|
|
54
|
+
*/
|
|
55
|
+
function checkStyle(content) {
|
|
56
|
+
if (!content || typeof content !== 'string') return { violations: [], hasDash: false, hasEmoji: false };
|
|
57
|
+
|
|
58
|
+
const violations = [];
|
|
59
|
+
const hasDash = EM_DASH_PATTERN.test(content);
|
|
60
|
+
const hasEmoji = EMOJI_PATTERN.test(content);
|
|
61
|
+
|
|
62
|
+
if (hasDash) {
|
|
63
|
+
violations.push('Em-dash or en-dash detected. Use commas, semicolons, colons, or periods instead (Article V).');
|
|
64
|
+
}
|
|
65
|
+
if (hasEmoji) {
|
|
66
|
+
violations.push('Emoji detected. Use icons from approved libraries instead (Directive 1).');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return { violations, hasDash, hasEmoji };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Check bash command for style violations in commit messages / PR descriptions.
|
|
74
|
+
*/
|
|
75
|
+
function checkBashStyle(command) {
|
|
76
|
+
if (!command) return { violations: [], hasDash: false, hasEmoji: false };
|
|
77
|
+
|
|
78
|
+
// Only check git commit and PR commands
|
|
79
|
+
const isGitOutput = /git\s+commit|gh\s+pr\s+create/i.test(command);
|
|
80
|
+
if (!isGitOutput) return { violations: [], hasDash: false, hasEmoji: false };
|
|
81
|
+
|
|
82
|
+
return checkStyle(command);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function main() {
|
|
86
|
+
let input = '';
|
|
87
|
+
for await (const chunk of process.stdin) {
|
|
88
|
+
input += chunk;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
const event = JSON.parse(input);
|
|
93
|
+
const projectDir = event.cwd || process.cwd();
|
|
94
|
+
const toolName = event.tool_name || '';
|
|
95
|
+
const toolInput = event.tool_input || {};
|
|
96
|
+
|
|
97
|
+
// Skip exempt paths
|
|
98
|
+
const filePath = toolInput.file_path || '';
|
|
99
|
+
if (isExemptPath(filePath, projectDir)) {
|
|
100
|
+
process.stdout.write(JSON.stringify({ decision: 'allow' }));
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
let result = { violations: [], hasDash: false, hasEmoji: false };
|
|
105
|
+
|
|
106
|
+
if (toolName === 'Write' || toolName === 'Edit') {
|
|
107
|
+
const content = toolInput.content || toolInput.new_string || '';
|
|
108
|
+
result = checkStyle(content);
|
|
109
|
+
} else if (toolName === 'Bash') {
|
|
110
|
+
const command = toolInput.command || '';
|
|
111
|
+
result = checkBashStyle(command);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (result.violations.length > 0) {
|
|
115
|
+
process.stdout.write(JSON.stringify({
|
|
116
|
+
decision: 'allow',
|
|
117
|
+
reason: `[Style Guard] ${result.violations.join(' ')}`,
|
|
118
|
+
}));
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
process.stdout.write(JSON.stringify({ decision: 'allow' }));
|
|
123
|
+
} catch (err) {
|
|
124
|
+
process.stderr.write(`[chati] style-guard: ${err.message}\n`);
|
|
125
|
+
process.stdout.write(JSON.stringify({ decision: 'allow' }));
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export { checkStyle, checkBashStyle, isExemptPath, EM_DASH_PATTERN, EMOJI_PATTERN };
|
|
130
|
+
|
|
131
|
+
import { fileURLToPath } from 'url';
|
|
132
|
+
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
133
|
+
main();
|
|
134
|
+
}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Undercover Guard Hook — PreToolUse (Write/Edit/Bash)
|
|
4
|
+
*
|
|
5
|
+
* Inspired by Claude Code's Undercover Mode: automatically sanitizes
|
|
6
|
+
* internal chati.dev references from generated code, commits, and PRs
|
|
7
|
+
* so end-user deliverables don't expose framework internals.
|
|
8
|
+
*
|
|
9
|
+
* Purpose: When users ship code to clients, open-source, or production,
|
|
10
|
+
* their outputs should look like professional developer work — not
|
|
11
|
+
* reveal the orchestration framework behind it.
|
|
12
|
+
*
|
|
13
|
+
* What gets sanitized:
|
|
14
|
+
* - Agent names (greenfield-wu, brownfield-wu, qa-planning, etc.)
|
|
15
|
+
* - Pipeline terminology (PRISM, L0-L5, deviation protocol)
|
|
16
|
+
* - Internal paths (chati.dev/artifacts/, .chati/)
|
|
17
|
+
* - Framework name references in generated code/commits
|
|
18
|
+
* - Constitution/Article references
|
|
19
|
+
* - Quality gate internals (adversarial review, devil's advocate)
|
|
20
|
+
*
|
|
21
|
+
* What is NOT sanitized:
|
|
22
|
+
* - Files inside chati.dev/ directory (framework's own files)
|
|
23
|
+
* - Files inside .chati/ directory (session state)
|
|
24
|
+
* - User's CLAUDE.md (framework context file)
|
|
25
|
+
* - Hook files themselves
|
|
26
|
+
*
|
|
27
|
+
* Mode: Advisory (warn, not block). Injects cleaned content suggestion
|
|
28
|
+
* but does NOT block the operation. The agent should self-correct.
|
|
29
|
+
*
|
|
30
|
+
* Activation: Always active during BUILD and DEPLOY phases.
|
|
31
|
+
* Can be disabled via config.yaml: undercover_mode: false
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import { existsSync, readFileSync } from 'fs';
|
|
35
|
+
import { join } from 'path';
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Internal terms that should not appear in user-facing deliverables.
|
|
39
|
+
* Each entry: { pattern: RegExp, replacement: string, category: string }
|
|
40
|
+
*/
|
|
41
|
+
const UNDERCOVER_RULES = [
|
|
42
|
+
// Agent names → generic descriptions
|
|
43
|
+
{ pattern: /\bgreenfield-wu\b/gi, replacement: 'discovery analysis', category: 'agent' },
|
|
44
|
+
{ pattern: /\bbrownfield-wu\b/gi, replacement: 'codebase analysis', category: 'agent' },
|
|
45
|
+
{ pattern: /\bqa-planning\b/gi, replacement: 'planning review', category: 'agent' },
|
|
46
|
+
{ pattern: /\bqa-implementation\b/gi, replacement: 'code review', category: 'agent' },
|
|
47
|
+
{ pattern: /\bdevops agent\b/gi, replacement: 'deployment', category: 'agent' },
|
|
48
|
+
{ pattern: /\bdev agent\b/gi, replacement: 'implementation', category: 'agent' },
|
|
49
|
+
{ pattern: /\bbrief agent\b/gi, replacement: 'requirements analysis', category: 'agent' },
|
|
50
|
+
{ pattern: /\bdetail agent\b/gi, replacement: 'specification', category: 'agent' },
|
|
51
|
+
{ pattern: /\barchitect agent\b/gi, replacement: 'architecture design', category: 'agent' },
|
|
52
|
+
{ pattern: /\bux agent\b/gi, replacement: 'UX design', category: 'agent' },
|
|
53
|
+
{ pattern: /\bphases agent\b/gi, replacement: 'roadmap planning', category: 'agent' },
|
|
54
|
+
{ pattern: /\btasks agent\b/gi, replacement: 'task breakdown', category: 'agent' },
|
|
55
|
+
|
|
56
|
+
// Pipeline terminology
|
|
57
|
+
{ pattern: /\bPRISM (?:engine|context|L[0-5])\b/gi, replacement: 'context system', category: 'pipeline' },
|
|
58
|
+
{ pattern: /\bdeviation protocol\b/gi, replacement: 'change request', category: 'pipeline' },
|
|
59
|
+
{ pattern: /\btwo-layer handoff\b/gi, replacement: 'transition document', category: 'pipeline' },
|
|
60
|
+
{ pattern: /\bhandoff document\b/gi, replacement: 'transition document', category: 'pipeline' },
|
|
61
|
+
{ pattern: /\bcontext bracket\b/gi, replacement: 'context status', category: 'pipeline' },
|
|
62
|
+
{ pattern: /\bquality gate\b/gi, replacement: 'quality check', category: 'pipeline' },
|
|
63
|
+
{ pattern: /\badversarial review\b/gi, replacement: 'critical review', category: 'pipeline' },
|
|
64
|
+
{ pattern: /\bdevil'?s advocate\b/gi, replacement: 'critical analysis', category: 'pipeline' },
|
|
65
|
+
{ pattern: /\bRalph Wiggum\b/gi, replacement: 'autonomous mode', category: 'pipeline' },
|
|
66
|
+
|
|
67
|
+
// Framework references in generated content
|
|
68
|
+
{ pattern: /\bchati\.dev(?:\/|\s|$)/gi, replacement: 'framework ', category: 'framework' },
|
|
69
|
+
{ pattern: /\bchati-dev\b/gi, replacement: 'framework', category: 'framework' },
|
|
70
|
+
{ pattern: /\bConstitution Article [IVXLC]+\b/gi, replacement: 'governance rule', category: 'framework' },
|
|
71
|
+
{ pattern: /\bArticle [IVXLC]+(?:\s*[-:]\s*\w+)/gi, replacement: 'governance rule', category: 'framework' },
|
|
72
|
+
|
|
73
|
+
// Internal paths in comments/strings
|
|
74
|
+
{ pattern: /chati\.dev\/artifacts\/\S+/gi, replacement: 'project artifacts', category: 'path' },
|
|
75
|
+
{ pattern: /\.chati\/memories\/\S+/gi, replacement: 'project memory', category: 'path' },
|
|
76
|
+
{ pattern: /\.chati\/session\.yaml/gi, replacement: 'session state', category: 'path' },
|
|
77
|
+
|
|
78
|
+
// Blocker taxonomy (internal codes)
|
|
79
|
+
{ pattern: /\b[CG]\d{2}\s*[-:]\s*/gi, replacement: '', category: 'internal' },
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Paths that are exempt from undercover sanitization.
|
|
84
|
+
* These are the framework's own files — internal references are expected.
|
|
85
|
+
*/
|
|
86
|
+
const EXEMPT_PATH_PATTERNS = [
|
|
87
|
+
/^chati\.dev\//,
|
|
88
|
+
/^\.chati\//,
|
|
89
|
+
/^\.claude\//,
|
|
90
|
+
/CLAUDE\.md$/,
|
|
91
|
+
/CLAUDE\.local\.md$/,
|
|
92
|
+
/GEMINI\.md$/,
|
|
93
|
+
/AGENTS\.md$/,
|
|
94
|
+
];
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Check if a file path is exempt from undercover sanitization.
|
|
98
|
+
*/
|
|
99
|
+
function isExemptPath(filePath, projectDir) {
|
|
100
|
+
if (!filePath) return true;
|
|
101
|
+
const relative = filePath.startsWith(projectDir)
|
|
102
|
+
? filePath.slice(projectDir.length).replace(/^[/\\]/, '')
|
|
103
|
+
: filePath;
|
|
104
|
+
return EXEMPT_PATH_PATTERNS.some(p => p.test(relative));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Check if undercover mode is enabled.
|
|
109
|
+
* Default: true (enabled). Disable via config.yaml undercover_mode: false
|
|
110
|
+
*/
|
|
111
|
+
function isUndercoverEnabled(projectDir) {
|
|
112
|
+
try {
|
|
113
|
+
const configPath = join(projectDir, 'chati.dev', 'config.yaml');
|
|
114
|
+
if (!existsSync(configPath)) return true;
|
|
115
|
+
const raw = readFileSync(configPath, 'utf-8');
|
|
116
|
+
const match = raw.match(/^\s*undercover_mode:\s*(false|no|off|0)/mi);
|
|
117
|
+
return !match;
|
|
118
|
+
} catch { /* expected: config may not exist — default to enabled */
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Scan content for internal references and return findings.
|
|
125
|
+
* @param {string} content - Text to scan
|
|
126
|
+
* @returns {{ found: boolean, matches: Array<{term: string, category: string}> }}
|
|
127
|
+
*/
|
|
128
|
+
function scanForInternals(content) {
|
|
129
|
+
if (!content || typeof content !== 'string') return { found: false, matches: [] };
|
|
130
|
+
|
|
131
|
+
const matches = [];
|
|
132
|
+
const seen = new Set();
|
|
133
|
+
|
|
134
|
+
for (const rule of UNDERCOVER_RULES) {
|
|
135
|
+
// Create fresh regex to avoid shared lastIndex state issues
|
|
136
|
+
const match = new RegExp(rule.pattern.source, rule.pattern.flags).exec(content);
|
|
137
|
+
if (match) {
|
|
138
|
+
const term = match[0];
|
|
139
|
+
if (!seen.has(term.toLowerCase())) {
|
|
140
|
+
seen.add(term.toLowerCase());
|
|
141
|
+
matches.push({ term, category: rule.category, replacement: rule.replacement });
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return { found: matches.length > 0, matches };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Check if a bash command contains internal references (commit messages, PR descriptions).
|
|
151
|
+
*/
|
|
152
|
+
function scanBashForInternals(command) {
|
|
153
|
+
if (!command) return { found: false, matches: [] };
|
|
154
|
+
|
|
155
|
+
// Only check git commit and PR-related commands
|
|
156
|
+
const isGitOutput = /git\s+(commit|push|pr|pull-request)|gh\s+(pr|issue)\s+create/i.test(command);
|
|
157
|
+
if (!isGitOutput) return { found: false, matches: [] };
|
|
158
|
+
|
|
159
|
+
return scanForInternals(command);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function main() {
|
|
163
|
+
let input = '';
|
|
164
|
+
for await (const chunk of process.stdin) {
|
|
165
|
+
input += chunk;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
try {
|
|
169
|
+
const event = JSON.parse(input);
|
|
170
|
+
const projectDir = event.cwd || process.cwd();
|
|
171
|
+
const toolName = event.tool_name || '';
|
|
172
|
+
const toolInput = event.tool_input || {};
|
|
173
|
+
|
|
174
|
+
// Check if undercover mode is enabled
|
|
175
|
+
if (!isUndercoverEnabled(projectDir)) {
|
|
176
|
+
process.stdout.write(JSON.stringify({ decision: 'allow' }));
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Skip framework-internal files
|
|
181
|
+
const filePath = toolInput.file_path || '';
|
|
182
|
+
if (isExemptPath(filePath, projectDir)) {
|
|
183
|
+
process.stdout.write(JSON.stringify({ decision: 'allow' }));
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
let scan = { found: false, matches: [] };
|
|
188
|
+
|
|
189
|
+
if (toolName === 'Write' || toolName === 'Edit') {
|
|
190
|
+
const content = toolInput.content || toolInput.new_string || '';
|
|
191
|
+
scan = scanForInternals(content);
|
|
192
|
+
} else if (toolName === 'Bash') {
|
|
193
|
+
const command = toolInput.command || '';
|
|
194
|
+
scan = scanBashForInternals(command);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (scan.found) {
|
|
198
|
+
const terms = scan.matches.map(m => `"${m.term}" → "${m.replacement}"`).join(', ');
|
|
199
|
+
process.stdout.write(JSON.stringify({
|
|
200
|
+
decision: 'allow', // Advisory — don't block, just warn
|
|
201
|
+
reason: `[Undercover] Internal references detected in output: ${terms}. Please sanitize before shipping to production. Replace internal terms with their generic equivalents.`,
|
|
202
|
+
}));
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
process.stdout.write(JSON.stringify({ decision: 'allow' }));
|
|
207
|
+
} catch (err) {
|
|
208
|
+
process.stderr.write(`[chati-hook-error] undercover-guard: ${err?.message || 'unknown'}\n`);
|
|
209
|
+
// Fail-open: undercover is advisory, not security-critical
|
|
210
|
+
process.stdout.write(JSON.stringify({ decision: 'allow' }));
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export { scanForInternals, scanBashForInternals, isExemptPath, isUndercoverEnabled, UNDERCOVER_RULES };
|
|
215
|
+
|
|
216
|
+
// Only run main when executed directly (not imported by tests)
|
|
217
|
+
import { fileURLToPath } from 'url';
|
|
218
|
+
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
219
|
+
main();
|
|
220
|
+
}
|
package/framework/i18n/en.yaml
CHANGED
|
@@ -28,7 +28,7 @@ installer:
|
|
|
28
28
|
quick_start_switch_hint: "Switch CLIs anytime — your session continues from where you left off"
|
|
29
29
|
created_overlays: "Created provider overlay directories"
|
|
30
30
|
will_install: "Will install:"
|
|
31
|
-
agents_count: "
|
|
31
|
+
agents_count: "Agent definitions across DISCOVER, PLAN, BUILD, DEPLOY phases"
|
|
32
32
|
workflows_count: "7 workflow blueprints"
|
|
33
33
|
templates_count: "10 templates (PRD, Brownfield PRD, Architecture, Task, QA Gate, Quick Brief, Brandbook, Design Tokens, Component Spec, Icon System)"
|
|
34
34
|
constitution: "Constitution (19 Articles + Preamble)"
|
|
@@ -44,7 +44,7 @@ installer:
|
|
|
44
44
|
created_claude_md: "Created CLAUDE.md"
|
|
45
45
|
configured_mcps: "Configured MCPs:"
|
|
46
46
|
validating: "Validating installation..."
|
|
47
|
-
agents_valid: "All
|
|
47
|
+
agents_valid: "All agents implement 8 protocols"
|
|
48
48
|
handoff_ok: "Handoff protocol: OK"
|
|
49
49
|
validation_ok: "Self-validation criteria: OK"
|
|
50
50
|
constitution_ok: "Constitution: 19 articles verified"
|
package/framework/i18n/es.yaml
CHANGED
|
@@ -28,7 +28,7 @@ installer:
|
|
|
28
28
|
quick_start_switch_hint: "Cambie de CLI en cualquier momento — su sesion continua desde donde quedo"
|
|
29
29
|
created_overlays: "Creados directorios de overlay de proveedores"
|
|
30
30
|
will_install: "Se instalara:"
|
|
31
|
-
agents_count: "
|
|
31
|
+
agents_count: "Definiciones de agentes en las fases DISCOVER, PLAN, BUILD, DEPLOY"
|
|
32
32
|
workflows_count: "7 blueprints de workflow"
|
|
33
33
|
templates_count: "10 templates (PRD, PRD Brownfield, Arquitectura, Task, QA Gate, Quick Brief, Brandbook, Design Tokens, Component Spec, Icon System)"
|
|
34
34
|
constitution: "Constitucion (19 Articulos + Preambulo)"
|
|
@@ -44,7 +44,7 @@ installer:
|
|
|
44
44
|
created_claude_md: "Creado CLAUDE.md"
|
|
45
45
|
configured_mcps: "MCPs configurados:"
|
|
46
46
|
validating: "Validando instalacion..."
|
|
47
|
-
agents_valid: "
|
|
47
|
+
agents_valid: "Todos los agentes implementan 8 protocolos"
|
|
48
48
|
handoff_ok: "Protocolo de handoff: OK"
|
|
49
49
|
validation_ok: "Criterios de auto-validacion: OK"
|
|
50
50
|
constitution_ok: "Constitucion: 19 articulos verificados"
|
package/framework/i18n/fr.yaml
CHANGED
|
@@ -28,7 +28,7 @@ installer:
|
|
|
28
28
|
quick_start_switch_hint: "Changez de CLI a tout moment — votre session continue la ou vous l'avez laissee"
|
|
29
29
|
created_overlays: "Crees les repertoires d'overlay des fournisseurs"
|
|
30
30
|
will_install: "Sera installe:"
|
|
31
|
-
agents_count: "
|
|
31
|
+
agents_count: "Definitions d'agents dans les phases DISCOVER, PLAN, BUILD, DEPLOY"
|
|
32
32
|
workflows_count: "7 blueprints de workflow"
|
|
33
33
|
templates_count: "10 templates (PRD, PRD Brownfield, Architecture, Task, QA Gate, Quick Brief, Brandbook, Design Tokens, Component Spec, Icon System)"
|
|
34
34
|
constitution: "Constitution (19 Articles + Preambule)"
|
|
@@ -44,7 +44,7 @@ installer:
|
|
|
44
44
|
created_claude_md: "Cree CLAUDE.md"
|
|
45
45
|
configured_mcps: "MCPs configures:"
|
|
46
46
|
validating: "Validation de l'installation..."
|
|
47
|
-
agents_valid: "
|
|
47
|
+
agents_valid: "Tous les agents implementent 8 protocoles"
|
|
48
48
|
handoff_ok: "Protocole de passation: OK"
|
|
49
49
|
validation_ok: "Criteres d'auto-validation: OK"
|
|
50
50
|
constitution_ok: "Constitution: 19 articles verifies"
|
package/framework/i18n/pt.yaml
CHANGED
|
@@ -28,7 +28,7 @@ installer:
|
|
|
28
28
|
quick_start_switch_hint: "Troque de CLI a qualquer momento — sua sessao continua de onde parou"
|
|
29
29
|
created_overlays: "Criados diretorios de overlay de provedores"
|
|
30
30
|
will_install: "Sera instalado:"
|
|
31
|
-
agents_count: "
|
|
31
|
+
agents_count: "Definicoes de agentes nas fases DISCOVER, PLAN, BUILD, DEPLOY"
|
|
32
32
|
workflows_count: "7 blueprints de workflow"
|
|
33
33
|
templates_count: "10 templates (PRD, PRD Brownfield, Arquitetura, Task, QA Gate, Quick Brief, Brandbook, Design Tokens, Component Spec, Icon System)"
|
|
34
34
|
constitution: "Constituicao (19 Artigos + Preambulo)"
|
|
@@ -44,7 +44,7 @@ installer:
|
|
|
44
44
|
created_claude_md: "Criado CLAUDE.md"
|
|
45
45
|
configured_mcps: "MCPs configurados:"
|
|
46
46
|
validating: "Validando instalacao..."
|
|
47
|
-
agents_valid: "Todos os
|
|
47
|
+
agents_valid: "Todos os agentes implementam 8 protocolos"
|
|
48
48
|
handoff_ok: "Protocolo de handoff: OK"
|
|
49
49
|
validation_ok: "Criterios de auto-validacao: OK"
|
|
50
50
|
constitution_ok: "Constituicao: 19 artigos verificados"
|