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.
Files changed (107) hide show
  1. package/README.md +5 -0
  2. package/bin/chati.js +16 -0
  3. package/framework/agents/build/dev.md +43 -10
  4. package/framework/agents/discover/brief.md +38 -8
  5. package/framework/agents/discover/brownfield-wu.md +44 -3
  6. package/framework/agents/discover/greenfield-wu.md +14 -0
  7. package/framework/agents/plan/architect.md +6 -0
  8. package/framework/agents/plan/detail.md +25 -0
  9. package/framework/agents/plan/tasks.md +29 -1
  10. package/framework/agents/plan/ux-brand-architect.md +215 -0
  11. package/framework/agents/plan/ux-component-engineer.md +289 -0
  12. package/framework/agents/plan/ux-researcher.md +166 -0
  13. package/framework/agents/plan/ux.md +126 -1008
  14. package/framework/agents/quality/qa-implementation.md +121 -22
  15. package/framework/agents/quality/qa-planning.md +18 -0
  16. package/framework/config.yaml +15 -4
  17. package/framework/constitution.md +8 -2
  18. package/framework/context/root.md +1 -1
  19. package/framework/data/entity-registry.yaml +59 -3
  20. package/framework/hooks/constitution-guard.js +67 -1
  21. package/framework/hooks/license-guard.js +4 -4
  22. package/framework/hooks/model-governance.js +2 -1
  23. package/framework/hooks/prism-engine.js +74 -6
  24. package/framework/hooks/read-protection.js +1 -1
  25. package/framework/hooks/session-digest.js +159 -7
  26. package/framework/hooks/settings.json +16 -4
  27. package/framework/hooks/style-guard.js +134 -0
  28. package/framework/hooks/undercover-guard.js +220 -0
  29. package/framework/intelligence/context-engine.md +21 -0
  30. package/framework/intelligence/hooks-performance.md +54 -0
  31. package/framework/intelligence/memory-layer.md +47 -0
  32. package/framework/migrations/v4.0-to-v4.1.yaml +165 -0
  33. package/framework/orchestrator/chati.md +327 -1067
  34. package/framework/templates/brandbook-html-tmpl.md +107 -0
  35. package/framework/templates/session-memory-tmpl.yaml +68 -0
  36. package/package.json +1 -1
  37. package/src/autonomy/build-state.js +1 -1
  38. package/src/autonomy/worktree-manager.js +13 -13
  39. package/src/config/agent-customizer.js +1 -1
  40. package/src/config/gemini-hooks-generator.js +6 -6
  41. package/src/config/ide-configs.js +1 -1
  42. package/src/context/bracket-tracker.js +25 -4
  43. package/src/context/engine.js +37 -7
  44. package/src/context/formatter.js +45 -1
  45. package/src/context/index.js +3 -3
  46. package/src/dashboard/data-reader.js +7 -7
  47. package/src/decision/engine.js +2 -2
  48. package/src/decision/registry-healer.js +1 -1
  49. package/src/decision/registry-updater.js +2 -2
  50. package/src/extensions/loader.js +1 -1
  51. package/src/gates/g1-planning-complete.js +1 -1
  52. package/src/gates/g2-qa-planning.js +1 -1
  53. package/src/gates/g3-implementation.js +4 -4
  54. package/src/gates/g4-qa-implementation.js +2 -2
  55. package/src/health/auto-fix.js +3 -3
  56. package/src/health/engine.js +2 -2
  57. package/src/installer/brownfield-upgrader.js +3 -3
  58. package/src/installer/manifest.js +1 -1
  59. package/src/installer/transaction.js +1 -1
  60. package/src/installer/validator.js +2 -2
  61. package/src/intelligence/decision-engine.js +1 -1
  62. package/src/intelligence/memory-manager.js +1 -1
  63. package/src/intelligence/registry-manager.js +2 -2
  64. package/src/intelligence/timeline.js +1 -1
  65. package/src/license/client.js +1 -1
  66. package/src/license/commands.js +2 -2
  67. package/src/memory/agent-memory.js +3 -3
  68. package/src/memory/daily-digest.js +170 -0
  69. package/src/memory/dream.js +254 -0
  70. package/src/memory/gotchas.js +2 -2
  71. package/src/memory/index.js +18 -0
  72. package/src/memory/magic-docs.js +98 -0
  73. package/src/memory/memory-extractor.js +163 -0
  74. package/src/memory/session-digest.js +144 -6
  75. package/src/merger/yaml-merger.js +1 -1
  76. package/src/orchestrator/cli.js +980 -0
  77. package/src/orchestrator/handoff-engine.js +25 -4
  78. package/src/orchestrator/index.js +9 -0
  79. package/src/orchestrator/session-manager.js +1 -1
  80. package/src/preview/detector.js +3 -3
  81. package/src/preview/launcher.js +2 -2
  82. package/src/quality/metrics-collector.js +1 -1
  83. package/src/quality/test-runner.js +2 -4
  84. package/src/scanning/density-scanner.js +51 -0
  85. package/src/scanning/env-scanner.js +97 -0
  86. package/src/scanning/index.js +7 -0
  87. package/src/scanning/leakage-scanner.js +54 -0
  88. package/src/scanning/placeholder-scanner.js +40 -0
  89. package/src/scanning/security-scanner.js +94 -0
  90. package/src/security/bash-security.js +335 -0
  91. package/src/security/index.js +9 -0
  92. package/src/telemetry/config.js +3 -3
  93. package/src/telemetry/sender.js +1 -1
  94. package/src/terminal/cli-registry.js +1 -1
  95. package/src/terminal/cost-tracker.js +1 -2
  96. package/src/terminal/prompt-builder.js +11 -27
  97. package/src/terminal/rate-limiter.js +1 -2
  98. package/src/terminal/run-agent.js +3 -3
  99. package/src/terminal/spawner.js +41 -4
  100. package/src/upgrade/checker.js +2 -2
  101. package/src/upgrade/migrator.js +1 -1
  102. package/src/utils/config-parser.js +1 -1
  103. package/src/utils/file-lock.js +3 -3
  104. package/src/utils/provider-limits.js +35 -4
  105. package/src/wizard/i18n.js +2 -2
  106. package/src/wizard/index.js +2 -1
  107. 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
- return {
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
- const content = Object.entries(digest)
48
- .map(([k, v]) => `${k}: "${v}"`)
49
- .join('\n');
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
- writeFileSync(join(digestDir, fileName), content + '\n', 'utf-8');
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)