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.
Files changed (116) hide show
  1. package/README.md +14 -9
  2. package/bin/chati.js +17 -1
  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-data-engineer.md +278 -0
  8. package/framework/agents/plan/architect-system.md +174 -0
  9. package/framework/agents/plan/architect.md +97 -301
  10. package/framework/agents/plan/detail.md +25 -0
  11. package/framework/agents/plan/tasks.md +29 -1
  12. package/framework/agents/plan/ux-brand-architect.md +215 -0
  13. package/framework/agents/plan/ux-component-engineer.md +289 -0
  14. package/framework/agents/plan/ux-researcher.md +166 -0
  15. package/framework/agents/plan/ux.md +126 -1008
  16. package/framework/agents/quality/qa-implementation.md +121 -22
  17. package/framework/agents/quality/qa-planning.md +18 -0
  18. package/framework/config.yaml +15 -4
  19. package/framework/constitution.md +9 -3
  20. package/framework/context/root.md +2 -2
  21. package/framework/data/entity-registry.yaml +59 -3
  22. package/framework/hooks/constitution-guard.js +67 -1
  23. package/framework/hooks/license-guard.js +4 -4
  24. package/framework/hooks/model-governance.js +2 -1
  25. package/framework/hooks/prism-engine.js +74 -6
  26. package/framework/hooks/read-protection.js +1 -1
  27. package/framework/hooks/session-digest.js +159 -7
  28. package/framework/hooks/settings.json +16 -4
  29. package/framework/hooks/style-guard.js +134 -0
  30. package/framework/hooks/undercover-guard.js +220 -0
  31. package/framework/i18n/en.yaml +2 -2
  32. package/framework/i18n/es.yaml +2 -2
  33. package/framework/i18n/fr.yaml +2 -2
  34. package/framework/i18n/pt.yaml +2 -2
  35. package/framework/intelligence/context-engine.md +21 -0
  36. package/framework/intelligence/hooks-performance.md +54 -0
  37. package/framework/intelligence/memory-layer.md +47 -0
  38. package/framework/migrations/v4.0-to-v4.1.yaml +165 -0
  39. package/framework/orchestrator/chati.md +327 -1067
  40. package/framework/schemas/session.schema.json +1 -1
  41. package/framework/templates/brandbook-html-tmpl.md +107 -0
  42. package/framework/templates/session-memory-tmpl.yaml +68 -0
  43. package/package.json +1 -1
  44. package/scripts/doctor/checks/agents.js +1 -1
  45. package/scripts/health-check.js +1 -1
  46. package/src/autonomy/build-state.js +1 -1
  47. package/src/autonomy/worktree-manager.js +13 -13
  48. package/src/config/agent-customizer.js +1 -1
  49. package/src/config/gemini-hooks-generator.js +149 -6
  50. package/src/config/ide-configs.js +1 -1
  51. package/src/context/bracket-tracker.js +25 -4
  52. package/src/context/engine.js +37 -7
  53. package/src/context/formatter.js +45 -1
  54. package/src/context/index.js +3 -3
  55. package/src/dashboard/data-reader.js +7 -7
  56. package/src/decision/engine.js +2 -2
  57. package/src/decision/registry-healer.js +1 -1
  58. package/src/decision/registry-updater.js +2 -2
  59. package/src/extensions/loader.js +1 -1
  60. package/src/gates/g1-planning-complete.js +1 -1
  61. package/src/gates/g2-qa-planning.js +1 -1
  62. package/src/gates/g3-implementation.js +4 -4
  63. package/src/gates/g4-qa-implementation.js +2 -2
  64. package/src/health/auto-fix.js +3 -3
  65. package/src/health/engine.js +2 -2
  66. package/src/installer/brownfield-upgrader.js +3 -3
  67. package/src/installer/manifest.js +1 -1
  68. package/src/installer/transaction.js +1 -1
  69. package/src/installer/validator.js +4 -4
  70. package/src/intelligence/decision-engine.js +1 -1
  71. package/src/intelligence/memory-manager.js +1 -1
  72. package/src/intelligence/registry-manager.js +2 -2
  73. package/src/intelligence/timeline.js +1 -1
  74. package/src/license/client.js +1 -1
  75. package/src/license/commands.js +2 -2
  76. package/src/memory/agent-memory.js +3 -3
  77. package/src/memory/daily-digest.js +170 -0
  78. package/src/memory/dream.js +254 -0
  79. package/src/memory/gotchas.js +2 -2
  80. package/src/memory/index.js +18 -0
  81. package/src/memory/magic-docs.js +98 -0
  82. package/src/memory/memory-extractor.js +163 -0
  83. package/src/memory/session-digest.js +144 -6
  84. package/src/merger/yaml-merger.js +1 -1
  85. package/src/orchestrator/cli.js +980 -0
  86. package/src/orchestrator/handoff-engine.js +31 -4
  87. package/src/orchestrator/index.js +9 -0
  88. package/src/orchestrator/session-manager.js +1 -1
  89. package/src/preview/detector.js +3 -3
  90. package/src/preview/launcher.js +2 -2
  91. package/src/quality/metrics-collector.js +1 -1
  92. package/src/quality/test-runner.js +2 -4
  93. package/src/scanning/density-scanner.js +51 -0
  94. package/src/scanning/env-scanner.js +97 -0
  95. package/src/scanning/index.js +7 -0
  96. package/src/scanning/leakage-scanner.js +54 -0
  97. package/src/scanning/placeholder-scanner.js +40 -0
  98. package/src/scanning/security-scanner.js +94 -0
  99. package/src/security/bash-security.js +335 -0
  100. package/src/security/index.js +9 -0
  101. package/src/telemetry/config.js +3 -3
  102. package/src/telemetry/sender.js +1 -1
  103. package/src/terminal/cli-registry.js +1 -1
  104. package/src/terminal/cost-tracker.js +1 -2
  105. package/src/terminal/prompt-builder.js +11 -27
  106. package/src/terminal/rate-limiter.js +1 -2
  107. package/src/terminal/run-agent.js +3 -3
  108. package/src/terminal/spawner.js +41 -4
  109. package/src/upgrade/checker.js +2 -2
  110. package/src/upgrade/migrator.js +1 -1
  111. package/src/utils/config-parser.js +1 -1
  112. package/src/utils/file-lock.js +3 -3
  113. package/src/utils/provider-limits.js +35 -4
  114. package/src/wizard/i18n.js +4 -4
  115. package/src/wizard/index.js +2 -1
  116. 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
+ }
@@ -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: "13 agent definitions (DISCOVER, PLAN, BUILD, DEPLOY phases)"
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 13 agents implement 8 protocols"
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"
@@ -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: "13 definiciones de agentes (fases DISCOVER, PLAN, BUILD, DEPLOY)"
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: "Los 13 agentes implementan 8 protocolos"
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"
@@ -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: "13 definitions d'agents (phases DISCOVER, PLAN, BUILD, DEPLOY)"
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: "Les 13 agents implementent 8 protocoles"
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"
@@ -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: "13 definicoes de agentes (fases DISCOVER, PLAN, BUILD, DEPLOY)"
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 13 agentes implementam 8 protocolos"
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"