chati-dev 4.0.11 → 4.1.0
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 +5 -0
- package/bin/chati.js +16 -0
- 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.md +6 -0
- 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 +8 -2
- package/framework/context/root.md +1 -1
- 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/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/templates/brandbook-html-tmpl.md +107 -0
- package/framework/templates/session-memory-tmpl.yaml +68 -0
- package/package.json +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 +6 -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 +2 -2
- 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 +25 -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 +2 -2
- 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
|
+
}
|
|
@@ -17,6 +17,14 @@ Four brackets define behavior based on remaining context:
|
|
|
17
17
|
| **DEPLETED** | 25-40% | 3 layers (L0-L2). Heavy reinforcement (4.0%) — significant context loss. Chunk memory recovery. |
|
|
18
18
|
| **CRITICAL** | <25% | 2 layers (L0-L1). Maximum reinforcement (5.0%) — last interactions before handoff. Full memory dump. |
|
|
19
19
|
|
|
20
|
+
### Static/Dynamic PRISM Boundary
|
|
21
|
+
|
|
22
|
+
The PRISM engine separates context into a **static prefix** (L0 Constitution + L1 Global rules) and a **dynamic suffix** (L2-L5, changes per turn). A `<!-- STATIC_BOUNDARY -->` marker separates them in the XML output.
|
|
23
|
+
|
|
24
|
+
This enables prompt cache reuse — the static prefix is identical across turns within the same mode, so the LLM provider can cache it and only reprocess the dynamic portion.
|
|
25
|
+
|
|
26
|
+
The static cache is invalidated when the governance mode changes (planning → build → deploy).
|
|
27
|
+
|
|
20
28
|
---
|
|
21
29
|
|
|
22
30
|
## Layered Context Injection
|
|
@@ -100,6 +108,19 @@ bracket = calculateBracket(100 - contextPercent)
|
|
|
100
108
|
|
|
101
109
|
The orchestrator recalculates the bracket before each agent interaction and adjusts injection accordingly.
|
|
102
110
|
|
|
111
|
+
### Token-Based Bracket Estimation
|
|
112
|
+
|
|
113
|
+
Context remaining percentage is estimated from actual prompt content size rather than conversation turn count:
|
|
114
|
+
|
|
115
|
+
```
|
|
116
|
+
estimatedTokens = promptText.length / 4
|
|
117
|
+
remainingPercent = (1 - estimatedTokens / providerLimit) * 100
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
This replaces the legacy heuristic (`turnCount / maxTurns`) which was inaccurate for sessions with large tool results or long agent outputs. The token-based approach scales correctly across providers (Claude 200K, Gemini 1M, Codex 128K).
|
|
121
|
+
|
|
122
|
+
Fallback: If prompt text is unavailable, the turn-count heuristic is used.
|
|
123
|
+
|
|
103
124
|
---
|
|
104
125
|
|
|
105
126
|
## Autonomous Context Recovery
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Hook Performance Reference
|
|
2
|
+
|
|
3
|
+
## Execution Model
|
|
4
|
+
|
|
5
|
+
Each hook runs as a separate Node.js process via stdin/stdout JSON protocol.
|
|
6
|
+
Hooks execute SEQUENTIALLY within the same event matcher.
|
|
7
|
+
Claude Code spawns each hook as a child process, waits for JSON output, then proceeds.
|
|
8
|
+
|
|
9
|
+
## Hook Registry (11 total)
|
|
10
|
+
|
|
11
|
+
| Event | Matcher | Hook | Purpose |
|
|
12
|
+
|-------|---------|------|---------|
|
|
13
|
+
| UserPromptSubmit | `.*` | license-guard.js | License validation + session ping |
|
|
14
|
+
| UserPromptSubmit | `.*` | prism-engine.js | Context injection (bracket, memory, frustration) |
|
|
15
|
+
| UserPromptSubmit | `.*` | model-governance.js | Model assignment advisory |
|
|
16
|
+
| PreCompact | `.*` | session-digest.js | Session state capture + auto-dream + daily digest |
|
|
17
|
+
| PreToolUse | `Read` | read-protection.js | Block reading sensitive files |
|
|
18
|
+
| PreToolUse | `Write\|Edit` | mode-governance.js | Write scope enforcement by phase |
|
|
19
|
+
| PreToolUse | `Write\|Edit` | constitution-guard.js | Secret detection + 23 shell injection checks |
|
|
20
|
+
| PreToolUse | `Write\|Edit` | undercover-guard.js | Sanitize framework terms from deliverables |
|
|
21
|
+
| PreToolUse | `Write\|Edit` | style-guard.js | Em-dash and emoji enforcement |
|
|
22
|
+
| PreToolUse | `Bash` | constitution-guard.js | Destructive command + shell injection checks |
|
|
23
|
+
| PreToolUse | `Bash` | undercover-guard.js | Sanitize framework terms from git commits |
|
|
24
|
+
| PreToolUse | `Bash` | style-guard.js | Em-dash and emoji in commit messages |
|
|
25
|
+
|
|
26
|
+
## Latency Budget
|
|
27
|
+
|
|
28
|
+
| Event | Hook Count | Expected Latency | Notes |
|
|
29
|
+
|-------|-----------|-----------------|-------|
|
|
30
|
+
| UserPromptSubmit | 3 | ~150ms | Runs before every user message |
|
|
31
|
+
| PreCompact | 1 | ~50-200ms | Runs rarely (context compaction). Auto-dream may add 5-10s if triggered |
|
|
32
|
+
| PreToolUse (Read) | 1 | ~30ms | Minimal overhead per file read |
|
|
33
|
+
| PreToolUse (Write/Edit) | 4 | ~200ms | Heaviest path. 4 sequential hooks |
|
|
34
|
+
| PreToolUse (Bash) | 3 | ~150ms | 23 regex checks in constitution-guard are negligible (<5ms) |
|
|
35
|
+
|
|
36
|
+
## Performance Characteristics
|
|
37
|
+
|
|
38
|
+
- **Fastest path**: Read (1 hook, ~30ms)
|
|
39
|
+
- **Heaviest path**: Write/Edit (4 hooks, ~200ms)
|
|
40
|
+
- **Most frequent**: UserPromptSubmit (3 hooks on every message)
|
|
41
|
+
- **Rarest**: PreCompact (1 hook, only on context compaction)
|
|
42
|
+
|
|
43
|
+
## Optimization Notes
|
|
44
|
+
|
|
45
|
+
1. constitution-guard's 23 shell injection regex checks run in <5ms total (all patterns compiled once)
|
|
46
|
+
2. undercover-guard and style-guard could be skipped in discover/plan phases (no code being written)
|
|
47
|
+
3. If latency becomes noticeable, consider merging undercover-guard + style-guard into one hook (same event, similar logic)
|
|
48
|
+
4. license-guard has a 5-minute ping throttle to avoid network latency on every message
|
|
49
|
+
|
|
50
|
+
## When to Worry
|
|
51
|
+
|
|
52
|
+
- If Write/Edit operations feel sluggish (>500ms added latency), profile hooks individually
|
|
53
|
+
- If auto-dream triggers during PreCompact and takes >10s, consider increasing the trigger threshold from 100 to 150 entries
|
|
54
|
+
- If UserPromptSubmit hooks add noticeable delay, license-guard's network call is the most likely culprit (has 3s timeout)
|