chati-dev 4.2.1 → 4.3.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 (222) hide show
  1. package/README.md +80 -53
  2. package/bin/chati.js +150 -5
  3. package/framework/agents/build/dev.md +509 -76
  4. package/framework/agents/deploy/devops.md +40 -25
  5. package/framework/agents/discover/brief.md +156 -22
  6. package/framework/agents/discover/brownfield-wu.md +24 -14
  7. package/framework/agents/discover/greenfield-wu.md +100 -14
  8. package/framework/agents/plan/architect-data-engineer.md +6 -6
  9. package/framework/agents/plan/architect-system.md +46 -12
  10. package/framework/agents/plan/architect.md +40 -20
  11. package/framework/agents/plan/detail.md +36 -24
  12. package/framework/agents/plan/phases.md +36 -26
  13. package/framework/agents/plan/tasks.md +114 -33
  14. package/framework/agents/plan/ux-brand-architect.md +240 -8
  15. package/framework/agents/plan/ux-component-engineer.md +28 -13
  16. package/framework/agents/plan/ux-researcher.md +7 -6
  17. package/framework/agents/plan/ux.md +55 -22
  18. package/framework/agents/quality/qa-implementation.md +143 -74
  19. package/framework/agents/quality/qa-planning.md +115 -42
  20. package/framework/agents/quality/qa-visual.md +439 -0
  21. package/framework/agents/shared/visualizer.md +128 -0
  22. package/framework/config.yaml +8 -7
  23. package/framework/constitution.md +127 -43
  24. package/framework/context/governance.md +12 -7
  25. package/framework/context/quality.md +6 -5
  26. package/framework/context/root.md +6 -6
  27. package/framework/data/entity-registry.yaml +377 -4
  28. package/framework/data/model-limits.json +19 -0
  29. package/framework/domains/agents/qa-visual.yaml +74 -0
  30. package/framework/domains/constitution.yaml +46 -2
  31. package/framework/domains/workflows/greenfield-fullstack.yaml +2 -2
  32. package/framework/hooks/advance-trigger.js +131 -0
  33. package/framework/hooks/brief-validator.js +83 -0
  34. package/framework/hooks/constitution-guard.js +91 -36
  35. package/framework/hooks/license-guard.js +129 -190
  36. package/framework/hooks/mode-governance.js +68 -16
  37. package/framework/hooks/model-governance.js +19 -8
  38. package/framework/hooks/package.json +3 -0
  39. package/framework/hooks/post-dev.js +207 -0
  40. package/framework/hooks/prism-engine.js +288 -105
  41. package/framework/hooks/read-protection.js +37 -9
  42. package/framework/hooks/reasoning-escalator.js +371 -0
  43. package/framework/hooks/reference-trigger.js +117 -0
  44. package/framework/hooks/session-digest.js +94 -20
  45. package/framework/hooks/settings.json +32 -1
  46. package/framework/hooks/style-guard.js +52 -13
  47. package/framework/hooks/team-quality-gate.js +58 -25
  48. package/framework/hooks/undercover-guard.js +34 -13
  49. package/framework/i18n/en.yaml +3 -3
  50. package/framework/i18n/es.yaml +3 -3
  51. package/framework/i18n/fr.yaml +3 -3
  52. package/framework/i18n/pt.yaml +3 -3
  53. package/framework/intelligence/context-engine.md +4 -5
  54. package/framework/intelligence/decision-engine.md +1 -1
  55. package/framework/intelligence/hooks-performance.md +3 -3
  56. package/framework/migrations/v1.0-to-v1.1.yaml +1 -1
  57. package/framework/migrations/v1.4-to-v2.0.yaml +11 -11
  58. package/framework/migrations/v4.0-to-v4.1.yaml +2 -2
  59. package/framework/migrations/v4.2-to-v4.3.yaml +29 -0
  60. package/framework/orchestrator/chati-router.js +387 -0
  61. package/framework/orchestrator/chati-update.md +40 -40
  62. package/framework/orchestrator/chati.md +318 -115
  63. package/framework/scaffold/motion-premium/README.md +89 -0
  64. package/framework/scaffold/motion-premium/app/globals.css.template +400 -0
  65. package/framework/scaffold/motion-premium/app/layout.tsx.template +110 -0
  66. package/framework/scaffold/motion-premium/components/animation/BackgroundCrossfadeProvider.tsx.template +170 -0
  67. package/framework/scaffold/motion-premium/components/animation/LenisProvider.tsx.template +49 -0
  68. package/framework/scaffold/motion-premium/components/animation/PageTransitionWrapper.tsx.template +83 -0
  69. package/framework/scaffold/motion-premium/components/animation/Preloader.tsx.template +171 -0
  70. package/framework/scaffold/motion-premium/components/ui/Container.tsx.template +69 -0
  71. package/framework/scaffold/motion-premium/components/ui/PageSection.tsx.template +74 -0
  72. package/framework/scaffold/motion-premium/lib/animations/gsap.ts.template +112 -0
  73. package/framework/scaffold/motion-premium/lib/animations/refreshCoordinator.ts.template +75 -0
  74. package/framework/scaffold/motion-premium/lib/animations/tokens.ts.template +119 -0
  75. package/framework/scaffold/motion-premium/lib/animations/useGsapContext.ts.template +92 -0
  76. package/framework/scaffold/motion-premium/lib/animations/useScrollSnapStepper.ts.template +265 -0
  77. package/framework/scaffold/motion-premium/lib/animations/useSmoothScroll.ts.template +67 -0
  78. package/framework/scaffold/motion-premium/lib/brand.ts.template +43 -0
  79. package/framework/scaffold/motion-premium/scaffold.yaml +174 -0
  80. package/framework/scaffold/motion-premium-3d/README.md +80 -0
  81. package/framework/scaffold/motion-premium-3d/app/(3d)/scroll-demo/ScrollDemoCanvas.tsx.template +81 -0
  82. package/framework/scaffold/motion-premium-3d/app/(3d)/scroll-demo/ScrollDemoClient.tsx.template +75 -0
  83. package/framework/scaffold/motion-premium-3d/app/(3d)/scroll-demo/page.tsx.template +26 -0
  84. package/framework/scaffold/motion-premium-3d/components/3d/CameraRig.tsx.template +100 -0
  85. package/framework/scaffold/motion-premium-3d/components/3d/CanvasProvider.tsx.template +85 -0
  86. package/framework/scaffold/motion-premium-3d/components/3d/InvalidateOnScroll.tsx.template +51 -0
  87. package/framework/scaffold/motion-premium-3d/components/3d/MeshCrossfade.tsx.template +79 -0
  88. package/framework/scaffold/motion-premium-3d/components/3d/ScrollCrossfade.tsx.template +88 -0
  89. package/framework/scaffold/motion-premium-3d/components/3d/ScrollScene.tsx.template +121 -0
  90. package/framework/scaffold/motion-premium-3d/components/webgl/SceneFallback.tsx.template +65 -0
  91. package/framework/scaffold/motion-premium-3d/components/webgl/WebGLContext.tsx.template +68 -0
  92. package/framework/scaffold/motion-premium-3d/lib/webgl/detect.ts.template +69 -0
  93. package/framework/scaffold/motion-premium-3d/scaffold.yaml +133 -0
  94. package/framework/schemas/session.schema.json +109 -21
  95. package/framework/scripts/reference-capture.js +430 -0
  96. package/framework/scripts/visual-qa.js +674 -0
  97. package/framework/tasks/orchestrator-handoff.md +1 -1
  98. package/framework/tasks/orchestrator-resume.md +1 -1
  99. package/framework/tasks/orchestrator-route.md +1 -1
  100. package/framework/tasks/orchestrator-status.md +3 -3
  101. package/framework/tasks/qa-planning-gate-define.md +1 -1
  102. package/framework/templates/brandbook-html-tmpl.md +1 -1
  103. package/framework/templates/brandbook-tmpl.yaml +1 -1
  104. package/framework/templates/component-spec-tmpl.yaml +1 -1
  105. package/framework/templates/design-token-tmpl.yaml +1 -1
  106. package/framework/templates/icon-system-tmpl.yaml +1 -1
  107. package/framework/templates/team-planning-tasks.yaml +6 -5
  108. package/framework/workflows/brownfield-discovery.yaml +2 -2
  109. package/framework/workflows/brownfield-fullstack.yaml +15 -11
  110. package/framework/workflows/brownfield-service.yaml +14 -10
  111. package/framework/workflows/brownfield-ui.yaml +15 -11
  112. package/framework/workflows/greenfield-fullstack.yaml +16 -13
  113. package/framework/workflows/quick-flow.yaml +3 -3
  114. package/framework/workflows/standard-flow.yaml +12 -9
  115. package/package.json +10 -5
  116. package/src/autonomy/autonomous-gate.js +1 -0
  117. package/src/autonomy/build-state.js +1 -2
  118. package/src/autonomy/progress-reporter.js +1 -1
  119. package/src/config/agent-customizer.js +11 -3
  120. package/src/config/claude-settings-generator.js +226 -0
  121. package/src/config/context-file-generator.js +41 -21
  122. package/src/config/framework-adapter.js +1 -0
  123. package/src/config/gemini-hooks-generator.js +76 -6
  124. package/src/config/mcp-configs.js +1 -0
  125. package/src/context/layers/l1-global.js +2 -1
  126. package/src/dashboard/data-reader.js +4 -3
  127. package/src/dashboard/layout.js +2 -1
  128. package/src/decision/analyzer.js +6 -30
  129. package/src/decision/engine.js +4 -28
  130. package/src/decision/registry-healer.js +3 -2
  131. package/src/decision/registry-updater.js +23 -14
  132. package/src/extensions/loader.js +2 -8
  133. package/src/gates/g1-planning-complete.js +2 -1
  134. package/src/gates/g2-qa-planning.js +2 -1
  135. package/src/gates/g3-implementation.js +2 -1
  136. package/src/gates/g4-qa-implementation.js +3 -2
  137. package/src/gates/g5-deploy-ready.js +2 -1
  138. package/src/health/engine.js +4 -3
  139. package/src/installer/core.js +557 -81
  140. package/src/installer/preflight.js +131 -0
  141. package/src/installer/provider-overlay.js +3 -3
  142. package/src/installer/scaffold-applier.js +358 -0
  143. package/src/installer/templates.js +52 -31
  144. package/src/installer/validator.js +17 -12
  145. package/src/intelligence/registry-manager.js +22 -29
  146. package/src/intelligence/timeline.js +11 -6
  147. package/src/license/commands.js +1 -1
  148. package/src/license/wait.js +102 -0
  149. package/src/memory/agent-memory.js +81 -0
  150. package/src/memory/dream.js +32 -1
  151. package/src/merger/replace-merger.js +28 -15
  152. package/src/orchestrator/agent-selector.js +2 -1
  153. package/src/orchestrator/cli.js +1908 -80
  154. package/src/orchestrator/doctor.js +270 -0
  155. package/src/orchestrator/handoff-engine.js +4 -3
  156. package/src/orchestrator/index.js +2 -0
  157. package/src/orchestrator/pipeline-manager.js +306 -15
  158. package/src/orchestrator/session-manager.js +331 -6
  159. package/src/tasks/handoff.js +3 -2
  160. package/src/telemetry/config.js +4 -3
  161. package/src/telemetry/schema.js +1 -0
  162. package/src/terminal/collector.js +3 -2
  163. package/src/terminal/index.js +1 -2
  164. package/src/terminal/isolation.js +52 -18
  165. package/src/terminal/prompt-builder.js +42 -25
  166. package/src/terminal/run-parallel.js +1 -1
  167. package/src/terminal/run-team.js +3 -3
  168. package/src/terminal/team-task-list.js +43 -4
  169. package/src/upgrade/backup.js +3 -2
  170. package/src/upgrade/checker.js +3 -2
  171. package/src/upgrade/migrator.js +65 -7
  172. package/src/upgrade/tracked-files-detector.js +86 -0
  173. package/src/upgrade/user-messages.js +94 -0
  174. package/src/utils/config-parser.js +2 -1
  175. package/src/utils/feature-flags.js +2 -1
  176. package/src/utils/flatten-entities.js +69 -0
  177. package/src/utils/framework-dir.js +16 -0
  178. package/src/utils/model-id.js +85 -0
  179. package/src/utils/provider-limits.js +84 -23
  180. package/src/utils/schema-validator.js +1 -1
  181. package/src/wizard/i18n.js +5 -4
  182. package/src/wizard/index.js +14 -0
  183. package/assets/logo - c/303/263pia.png +0 -0
  184. package/assets/logo.svg +0 -42
  185. package/assets/logo2.png +0 -0
  186. package/assets/social-preview.png +0 -0
  187. package/scripts/bundle-framework.js +0 -69
  188. package/scripts/changelog-generator.js +0 -222
  189. package/scripts/codebase-mapper.js +0 -728
  190. package/scripts/commit-message-generator.js +0 -167
  191. package/scripts/coverage-analyzer.js +0 -260
  192. package/scripts/dependency-analyzer.js +0 -280
  193. package/scripts/doctor/checks/agents.js +0 -77
  194. package/scripts/doctor/checks/constitution.js +0 -41
  195. package/scripts/doctor/checks/domain-alignment.js +0 -58
  196. package/scripts/doctor/checks/prism-layers.js +0 -84
  197. package/scripts/doctor/checks/registry.js +0 -55
  198. package/scripts/doctor/checks/schemas.js +0 -61
  199. package/scripts/doctor/fixes/reference-fix.js +0 -100
  200. package/scripts/doctor/fixes/registry-fix.js +0 -56
  201. package/scripts/doctor/index.js +0 -212
  202. package/scripts/framework-analyzer.js +0 -308
  203. package/scripts/generate-constitution-domain.js +0 -253
  204. package/scripts/generate-signing-key.js +0 -33
  205. package/scripts/health-check.js +0 -481
  206. package/scripts/ide-sync.js +0 -326
  207. package/scripts/performance-analyzer.js +0 -325
  208. package/scripts/plan-tracker.js +0 -278
  209. package/scripts/populate-entity-registry.js +0 -481
  210. package/scripts/pr-review.js +0 -317
  211. package/scripts/rollback-manager.js +0 -310
  212. package/scripts/semantic-lint.js +0 -328
  213. package/scripts/sign-manifest.js +0 -53
  214. package/scripts/stuck-detector.js +0 -343
  215. package/scripts/test-quality-assessment.js +0 -257
  216. package/scripts/validate-agents.js +0 -368
  217. package/scripts/validate-package.js +0 -367
  218. package/scripts/validate-tasks.js +0 -465
  219. package/src/autonomy/worktree-manager.js +0 -250
  220. package/src/intelligence/decision-engine.js +0 -256
  221. package/src/intelligence/document-sharder.js +0 -221
  222. package/src/intelligence/elicitation.js +0 -265
@@ -2,28 +2,133 @@
2
2
  /**
3
3
  * PRISM Engine Hook — UserPromptSubmit
4
4
  *
5
- * Injects PRISM context block into every user prompt.
6
- * Reads session state to determine bracket, agent, and mode,
7
- * then runs the PRISM pipeline to produce XML context.
5
+ * Injects PRISM context block into every user prompt, and — since the
6
+ * context-window-detection v1 refactor maintains the cumulative token
7
+ * counter and detected model state on session.yaml so downstream consumers
8
+ * (/chati status, session-digest) see a token-accurate picture.
9
+ *
10
+ * Why this rewrite:
11
+ * Gabriel reported false DEPLETED alerts at 13.6% of an Opus 1M window.
12
+ * The old primary path used turn-count (`session.turnCount / maxTurns`)
13
+ * scaled to a fixed 40-turn ceiling — token-blind and model-blind.
14
+ * On a 1M window, 40 turns ≈ 10% of context, but the heuristic reported
15
+ * 0%. This rewrite replaces the primary metric with token-based
16
+ * cumulative estimation and resolves the window from the active model.
17
+ *
18
+ * Detection chain for the active model (first hit wins):
19
+ * 1. process.env.CLAUDE_MODEL (Claude Code set)
20
+ * 2. event.session.model (future-proof for event payload)
21
+ * 3. session.active_model (written by /chati boot)
22
+ * 4. HOOK_AGENT_MODELS[currentAgent] (last-resort default)
23
+ * 5. null (provider fallback takes over)
24
+ *
25
+ * Context limits come from the shared JSON table at either
26
+ * `.chati.dev/data/model-limits.json` (installed) or
27
+ * `chati.dev/data/model-limits.json` (monorepo/dev). The table maps
28
+ * normalized keys — `opus`, `opus[1m]`, `sonnet`, `sonnet[1m]`, `haiku`,
29
+ * `pro`, `flash`, `codex` — to window sizes, distinguishing Opus default
30
+ * (200K) from the 1M variant explicitly.
8
31
  *
9
32
  * Claude Code Hook: triggers on every user message submission.
10
33
  */
11
34
 
12
- import { existsSync, readFileSync } from 'fs';
35
+ import { existsSync, readFileSync, writeFileSync, renameSync } from 'fs';
13
36
  import { join } from 'path';
14
37
 
15
- /**
16
- * Read session.yaml and extract key fields for PRISM.
17
- */
38
+ // ---------------------------------------------------------------------------
39
+ // Context window limits load from shared JSON, fall back to embedded.
40
+ // ---------------------------------------------------------------------------
41
+
42
+ function loadLimits(projectDir) {
43
+ const candidates = [
44
+ join(projectDir, '.chati.dev', 'data', 'model-limits.json'),
45
+ join(projectDir, 'chati.dev', 'data', 'model-limits.json'),
46
+ ];
47
+ for (const candidate of candidates) {
48
+ if (existsSync(candidate)) {
49
+ try {
50
+ return JSON.parse(readFileSync(candidate, 'utf-8'));
51
+ } catch { /* malformed — fall through */ }
52
+ }
53
+ }
54
+ // Last-resort fallback. Matches chati.dev/data/model-limits.json verbatim.
55
+ // Scan-stale check #25 guards parity between this block and the JSON.
56
+ return {
57
+ providers: { claude: 200_000, gemini: 1_000_000, codex: 128_000 },
58
+ models: {
59
+ opus: 200_000,
60
+ 'opus[1m]': 1_000_000,
61
+ sonnet: 200_000,
62
+ 'sonnet[1m]': 1_000_000,
63
+ haiku: 200_000,
64
+ pro: 1_000_000,
65
+ flash: 1_000_000,
66
+ codex: 128_000,
67
+ },
68
+ };
69
+ }
70
+
71
+ // ---------------------------------------------------------------------------
72
+ // Model normalization (duplicate of src/utils/model-id.js — hooks cannot
73
+ // import package modules. Keep in sync if parser changes.)
74
+ // ---------------------------------------------------------------------------
75
+
76
+ const FAMILIES = new Set(['opus', 'sonnet', 'haiku']);
77
+
78
+ function parseClaudeModelId(raw) {
79
+ if (typeof raw !== 'string') return null;
80
+ const s = raw.trim().toLowerCase();
81
+ if (!s) return null;
82
+ let variant = 'default';
83
+ let body = s;
84
+ const variantMatch = body.match(/[[(:]\s*(1m)\s*\)?\]?$/);
85
+ if (variantMatch) {
86
+ variant = variantMatch[1];
87
+ body = body.slice(0, variantMatch.index).trim();
88
+ }
89
+ let family = null;
90
+ for (const candidate of FAMILIES) {
91
+ if (new RegExp(`(^|-)${candidate}(-|$)`).test(body)) {
92
+ family = candidate;
93
+ break;
94
+ }
95
+ }
96
+ if (!family) return null;
97
+ const normalized = variant === 'default' ? family : `${family}[${variant}]`;
98
+ return { family, variant, normalized, provider: 'claude' };
99
+ }
100
+
101
+ function resolveLimit(limits, modelKey, provider) {
102
+ if (modelKey && limits.models[modelKey] != null) return limits.models[modelKey];
103
+ if (modelKey) {
104
+ const parsed = parseClaudeModelId(modelKey);
105
+ if (parsed && limits.models[parsed.normalized] != null) return limits.models[parsed.normalized];
106
+ if (parsed && limits.models[parsed.family] != null) return limits.models[parsed.family];
107
+ }
108
+ if (provider && limits.providers[provider] != null) return limits.providers[provider];
109
+ return 200_000;
110
+ }
111
+
112
+ // ---------------------------------------------------------------------------
113
+ // Session read — lightweight YAML extraction (avoids js-yaml dependency).
114
+ // ---------------------------------------------------------------------------
115
+
18
116
  function readSessionState(projectDir) {
19
117
  const sessionPath = join(projectDir, '.chati', 'session.yaml');
20
118
  if (!existsSync(sessionPath)) return null;
21
119
 
22
120
  const raw = readFileSync(sessionPath, 'utf-8');
23
- // Lightweight YAML extraction (avoid dependency)
24
121
  const extract = (key) => {
25
122
  const match = raw.match(new RegExp(`^\\s*${key}:\\s*(.+)$`, 'm'));
26
- return match ? match[1].trim().replace(/^["']|["']$/g, '') : null;
123
+ if (!match) return null;
124
+ const v = match[1].trim().replace(/^["']|["']$/g, '');
125
+ return v === 'null' || v === '' ? null : v;
126
+ };
127
+ const extractNum = (key) => {
128
+ const v = extract(key);
129
+ if (v === null) return null;
130
+ const n = parseInt(v, 10);
131
+ return Number.isFinite(n) ? n : null;
27
132
  };
28
133
 
29
134
  // Detect active team (Article XXI)
@@ -38,26 +143,131 @@ function readSessionState(projectDir) {
38
143
  }
39
144
 
40
145
  return {
146
+ raw,
147
+ sessionPath,
41
148
  mode: extract('mode') || 'discover',
42
149
  currentAgent: extract('current_agent') || null,
43
150
  workflow: extract('workflow') || null,
44
151
  pipelinePosition: extract('pipeline_position') || null,
45
- turnCount: parseInt(extract('turn_count') || '0', 10),
46
- provider: extract('provider') || 'claude',
152
+ provider: extract('active_provider') || extract('provider') || 'claude',
153
+ // v1.2 context detection fields.
154
+ activeModel: extract('active_model'),
155
+ contextTokensUsed: extractNum('context_tokens_used') || 0,
156
+ contextWindowTokens: extractNum('context_window_tokens'),
47
157
  activeTeamId,
48
158
  activeTeamPhase,
49
159
  };
50
160
  }
51
161
 
52
- /**
53
- * Main hook handler.
54
- * Reads stdin for hook event, outputs context to inject.
55
- */
162
+ // ---------------------------------------------------------------------------
163
+ // Session write targeted line replacement on session.yaml.
164
+ // Atomic via tmp+rename. Only touches the fields we own.
165
+ // ---------------------------------------------------------------------------
166
+
167
+ function writeBackSession(sessionPath, rawContent, updates) {
168
+ let out = rawContent;
169
+ for (const [key, value] of Object.entries(updates)) {
170
+ const serialized =
171
+ value === null || value === undefined ? 'null' :
172
+ typeof value === 'number' ? String(value) :
173
+ typeof value === 'string' ? value :
174
+ JSON.stringify(value);
175
+ const re = new RegExp(`^(\\s*${key}:\\s*)(.*)$`, 'm');
176
+ if (re.test(out)) {
177
+ out = out.replace(re, `$1${serialized}`);
178
+ } else {
179
+ // Key not present — append at end (migration path).
180
+ out += (out.endsWith('\n') ? '' : '\n') + `${key}: ${serialized}\n`;
181
+ }
182
+ }
183
+ const tmpPath = sessionPath + '.tmp';
184
+ writeFileSync(tmpPath, out, 'utf-8');
185
+ renameSync(tmpPath, sessionPath);
186
+ }
187
+
188
+ // ---------------------------------------------------------------------------
189
+ // Model detection chain.
190
+ // ---------------------------------------------------------------------------
191
+
192
+ const HOOK_AGENT_MODELS = {
193
+ orchestrator: 'sonnet', 'greenfield-wu': 'haiku', 'brownfield-wu': 'opus',
194
+ brief: 'sonnet', detail: 'opus', architect: 'opus', ux: 'sonnet',
195
+ phases: 'sonnet', tasks: 'sonnet', 'qa-planning': 'opus',
196
+ 'qa-implementation': 'opus', 'qa-visual': 'opus', dev: 'opus', devops: 'sonnet',
197
+ };
198
+
199
+ function detectModel(event, session) {
200
+ // 1. Environment override — Claude Code or user-set.
201
+ if (process.env.CLAUDE_MODEL) {
202
+ const parsed = parseClaudeModelId(process.env.CLAUDE_MODEL);
203
+ if (parsed) return parsed.normalized;
204
+ return process.env.CLAUDE_MODEL; // trust the raw string
205
+ }
206
+ // 2. Event payload (future-proof — Claude Code may populate this).
207
+ if (event && event.session && event.session.model) {
208
+ const parsed = parseClaudeModelId(event.session.model);
209
+ if (parsed) return parsed.normalized;
210
+ }
211
+ // 3. Session-recorded active model — written at /chati boot.
212
+ if (session && session.activeModel) return session.activeModel;
213
+ // 4. Agent default (last resort, matches historic behavior).
214
+ if (session && session.currentAgent && HOOK_AGENT_MODELS[session.currentAgent]) {
215
+ return HOOK_AGENT_MODELS[session.currentAgent];
216
+ }
217
+ return null;
218
+ }
219
+
220
+ // ---------------------------------------------------------------------------
221
+ // Token accounting.
222
+ // Token-length heuristic: 1 token ≈ 4 characters (Claude's own rough rule).
223
+ // Tool-output multiplier: each UserPromptSubmit represents roughly one
224
+ // user-prompt + a mixed bag of preceding tool responses. We add a 3x
225
+ // multiplier on the user prompt to approximate that overhead, clamped at
226
+ // the window size.
227
+ // ---------------------------------------------------------------------------
228
+
229
+ const TOOL_OUTPUT_MULTIPLIER = 3;
230
+
231
+ function estimatePromptDelta(promptText) {
232
+ if (!promptText) return 0;
233
+ const baseTokens = Math.ceil(promptText.length / 4);
234
+ return baseTokens * (1 + TOOL_OUTPUT_MULTIPLIER);
235
+ }
236
+
237
+ function computeBracket(remainingPercent) {
238
+ if (remainingPercent < 25) return 'CRITICAL';
239
+ if (remainingPercent < 40) return 'DEPLETED';
240
+ if (remainingPercent < 60) return 'MODERATE';
241
+ return 'FRESH';
242
+ }
243
+
244
+ // ---------------------------------------------------------------------------
245
+ // Frustration detection — unchanged.
246
+ // ---------------------------------------------------------------------------
247
+
248
+ const FRUSTRATION_PATTERNS = [
249
+ /this (is|isn't|isnt) working/i,
250
+ /I (already|just) (told|said|asked)/i,
251
+ /why (won't|doesn't|can't|isn't|wont|doesnt|cant|isnt)/i,
252
+ /stop (doing|repeating|ignoring)/i,
253
+ /you('re| are) (not|never) (listening|reading|understanding)/i,
254
+ /for the (second|third|fourth|last) time/i,
255
+ /!!+/,
256
+ /\b(WTF|WHAT THE|FFS|OMG)\b/i,
257
+ ];
258
+
259
+ function detectFrustration(prompt) {
260
+ if (!prompt || prompt.length < 5) return false;
261
+ return FRUSTRATION_PATTERNS.some(p => p.test(prompt));
262
+ }
263
+
264
+ // ---------------------------------------------------------------------------
265
+ // Main handler.
266
+ // ---------------------------------------------------------------------------
267
+
56
268
  async function main() {
57
269
  let input = '';
58
- for await (const chunk of process.stdin) {
59
- input += chunk;
60
- }
270
+ for await (const chunk of process.stdin) input += chunk;
61
271
 
62
272
  try {
63
273
  const event = JSON.parse(input);
@@ -65,72 +275,55 @@ async function main() {
65
275
  const session = readSessionState(projectDir);
66
276
 
67
277
  if (!session) {
68
- // No active session — don't inject anything
69
- process.stdout.write(JSON.stringify({ result: 'allow' }));
278
+ process.stdout.write('{}');
70
279
  return;
71
280
  }
72
281
 
73
- // Estimate remaining context — prefer token-based estimation over turn count
74
- // Token estimation: 1 token ≈ 4 characters (Claude Code pattern)
75
- // SYNC: These limits MUST match packages/chati-dev/src/utils/provider-limits.js
76
- const HOOK_MODEL_LIMITS = { opus: 1_000_000, sonnet: 200_000, haiku: 200_000, pro: 1_000_000, flash: 1_000_000, codex: 128_000 };
77
- const HOOK_PROVIDER_LIMITS = { claude: 200_000, gemini: 1_000_000, codex: 128_000 };
78
-
79
- // Infer model from current agent via AGENT_MODELS map
80
- const HOOK_AGENT_MODELS = {
81
- orchestrator: 'sonnet', 'greenfield-wu': 'haiku', 'brownfield-wu': 'opus',
82
- brief: 'sonnet', detail: 'opus', architect: 'opus', ux: 'sonnet',
83
- phases: 'sonnet', tasks: 'sonnet', 'qa-planning': 'opus',
84
- 'qa-implementation': 'opus', dev: 'opus', devops: 'sonnet',
85
- };
86
- const inferredModel = session.currentAgent ? HOOK_AGENT_MODELS[session.currentAgent] : null;
87
-
88
- // Resolve context limit: model > provider > default
89
- function hookResolveLimit(model, provider) {
90
- if (model && HOOK_MODEL_LIMITS[model]) return HOOK_MODEL_LIMITS[model];
91
- if (provider && HOOK_PROVIDER_LIMITS[provider]) return HOOK_PROVIDER_LIMITS[provider];
92
- return 200_000;
93
- }
282
+ const limits = loadLimits(projectDir);
94
283
 
95
- // Primary: turn-count tracks cumulative context consumption across the session.
96
- // Fallback: prompt-length for first turn or missing session data.
284
+ // 1. Model + window detection.
285
+ const detectedModel = detectModel(event, session);
286
+ const contextLimit = resolveLimit(limits, detectedModel, session.provider);
287
+
288
+ // 2. Token accounting — cumulative, replaces turn-count.
97
289
  const promptText = event.prompt || '';
98
- let remainingPercent;
99
- const maxTurns = 40;
100
- if (session.turnCount > 0) {
101
- remainingPercent = Math.max(0, Math.round((1 - session.turnCount / maxTurns) * 100));
102
- } else {
103
- const estimatedTokens = Math.ceil(promptText.length / 4);
104
- const contextLimit = hookResolveLimit(inferredModel, session.provider);
105
- remainingPercent = Math.max(0, Math.round((1 - estimatedTokens / contextLimit) * 100));
106
- }
290
+ const delta = estimatePromptDelta(promptText);
291
+ const tokensUsed = Math.min(contextLimit, session.contextTokensUsed + delta);
292
+ const remainingPercent = Math.max(0, Math.round((1 - tokensUsed / contextLimit) * 100));
107
293
 
108
- // Determine bracket
109
- let bracket = 'FRESH';
110
- if (remainingPercent < 25) bracket = 'CRITICAL';
111
- else if (remainingPercent < 40) bracket = 'DEPLETED';
112
- else if (remainingPercent < 60) bracket = 'MODERATE';
294
+ // 3. Bracket computation (percentage-based, model-scaled).
295
+ const bracket = computeBracket(remainingPercent);
113
296
 
114
- // Load agent memory (if active agent has MEMORY.md)
297
+ // 4. Persist back to session.yaml (best-effort, non-fatal on write error).
298
+ try {
299
+ writeBackSession(session.sessionPath, session.raw, {
300
+ context_tokens_used: tokensUsed,
301
+ context_window_tokens: contextLimit,
302
+ context_last_bracket: bracket,
303
+ // If we detected a model and the session did not have one, cache it.
304
+ ...(detectedModel && !session.activeModel ? { active_model: detectedModel } : {}),
305
+ });
306
+ } catch (err) {
307
+ process.stderr.write(`[chati] prism-engine write-back failed (non-fatal): ${err.message}\n`);
308
+ }
309
+
310
+ // 5. Agent-local memory block (unchanged).
115
311
  let memoryBlock = '';
116
312
  if (session.currentAgent) {
117
313
  const memoryPath = join(projectDir, '.chati', 'memories', session.currentAgent, 'MEMORY.md');
118
314
  if (existsSync(memoryPath)) {
119
315
  const raw = readFileSync(memoryPath, 'utf-8').trim();
120
316
  if (raw) {
121
- const trimmed = raw.slice(0, 500);
122
- memoryBlock = ` <agent-memory agent="${session.currentAgent}">\n${trimmed}\n </agent-memory>`;
317
+ memoryBlock = ` <agent-memory agent="${session.currentAgent}">\n${raw.slice(0, 500)}\n </agent-memory>`;
123
318
  }
124
319
  }
125
320
  }
126
321
 
127
- // L6 Team Roster Layer (Article XII, Article XXI) — injected when a team is active
128
- // Max 500 tokens, flat budget. Contains team metadata + shared task status only.
322
+ // 6. Team context block (unchanged).
129
323
  let teamContextBlock = '';
130
324
  if (session.activeTeamId && bracket !== 'CRITICAL') {
131
325
  const taskListPath = join(projectDir, '.chati', 'teams', session.activeTeamId, 'tasks.yaml');
132
326
  if (existsSync(taskListPath)) {
133
- // Read and cap at 800 chars (~200 tokens) to stay within 500-token budget
134
327
  const rawTasks = readFileSync(taskListPath, 'utf-8').trim().slice(0, 800);
135
328
  teamContextBlock = [
136
329
  ` <team-context team-id="${session.activeTeamId}" phase="${session.activeTeamPhase}">`,
@@ -140,18 +333,16 @@ async function main() {
140
333
  }
141
334
  }
142
335
 
143
- // Frustration detection — adapt response style when user is frustrated
336
+ // 7. Advisories.
144
337
  const frustrationDetected = detectFrustration(promptText);
338
+ const microcompactAdvisory =
339
+ (bracket === 'DEPLETED' || bracket === 'CRITICAL')
340
+ ? ' <advisory priority="medium">Context constrained. Avoid re-reading files already in context. Summarize stale outputs before proceeding.</advisory>'
341
+ : '';
145
342
 
146
- // Microcompact advisory hint to economize context when depleted + idle
147
- let microcompactAdvisory = '';
148
- if ((bracket === 'DEPLETED' || bracket === 'CRITICAL') && session.turnCount > 10) {
149
- microcompactAdvisory = ' <advisory priority="medium">Context constrained. Avoid re-reading files already in context. Summarize stale outputs before proceeding.</advisory>';
150
- }
151
-
152
- // Build minimal context block (full PRISM pipeline is used by orchestrator internally)
343
+ // 8. Compose the context block.
153
344
  const contextBlock = [
154
- `<chati-context bracket="${bracket}">`,
345
+ `<chati-context bracket="${bracket}" model="${detectedModel || 'unknown'}" window="${contextLimit}" used="${tokensUsed}">`,
155
346
  ` <mode>${session.mode}</mode>`,
156
347
  session.currentAgent ? ` <agent>${session.currentAgent}</agent>` : '',
157
348
  session.pipelinePosition ? ` <pipeline-position>${session.pipelinePosition}</pipeline-position>` : '',
@@ -164,45 +355,37 @@ async function main() {
164
355
  ].filter(Boolean).join('\n');
165
356
 
166
357
  process.stdout.write(JSON.stringify({
167
- result: 'allow',
168
- prefix: contextBlock,
358
+ hookSpecificOutput: {
359
+ hookEventName: 'UserPromptSubmit',
360
+ additionalContext: contextBlock,
361
+ },
169
362
  }));
170
363
  } catch (err) {
171
364
  process.stderr.write(`[chati] prism-engine: ${err.message}\n`);
172
- process.stdout.write(JSON.stringify({ result: 'allow' }));
365
+ process.stdout.write('{}');
173
366
  }
174
367
  }
175
368
 
176
- /**
177
- * Frustration detection patterns.
178
- * Inspired by Claude Code's regex-based emotion detection.
179
- * Soft advisory only — never blocks or overrides behavior.
180
- */
181
- const FRUSTRATION_PATTERNS = [
182
- /this (is|isn't|isnt) working/i,
183
- /I (already|just) (told|said|asked)/i,
184
- /why (won't|doesn't|can't|isn't|wont|doesnt|cant|isnt)/i,
185
- /stop (doing|repeating|ignoring)/i,
186
- /you('re| are) (not|never) (listening|reading|understanding)/i,
187
- /for the (second|third|fourth|last) time/i,
188
- /!!+/,
189
- /\b(WTF|WHAT THE|FFS|OMG)\b/i,
190
- ];
369
+ export {
370
+ readSessionState,
371
+ detectFrustration,
372
+ detectModel,
373
+ parseClaudeModelId,
374
+ resolveLimit,
375
+ loadLimits,
376
+ estimatePromptDelta,
377
+ computeBracket,
378
+ writeBackSession,
379
+ TOOL_OUTPUT_MULTIPLIER,
380
+ };
191
381
 
192
- /**
193
- * Detect user frustration from prompt text.
194
- * @param {string} prompt - User prompt text
195
- * @returns {boolean} True if frustration patterns detected
196
- */
197
- function detectFrustration(prompt) {
198
- if (!prompt || prompt.length < 5) return false;
199
- return FRUSTRATION_PATTERNS.some(p => p.test(prompt));
200
- }
201
-
202
- export { readSessionState, detectFrustration };
203
-
204
- // Only run main when executed directly (not imported by tests)
382
+ // Only run main when executed directly (not imported by tests).
205
383
  import { fileURLToPath } from 'url';
206
- if (process.argv[1] === fileURLToPath(import.meta.url)) {
207
- main();
384
+ import { realpathSync } from 'fs';
385
+ if (process.argv[1]) {
386
+ try {
387
+ if (realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url))) {
388
+ main();
389
+ }
390
+ } catch { /* path resolution failed — not invoked as a script */ }
208
391
  }
@@ -93,24 +93,52 @@ async function main() {
93
93
  const result = isSensitivePath(filePath, cwd);
94
94
 
95
95
  if (result.sensitive) {
96
- process.stdout.write(JSON.stringify({
97
- decision: 'block',
98
- reason: `[Article IV] ${result.reason}. Use environment variables or secure vaults instead of reading sensitive files directly.`,
99
- }));
96
+ process.stdout.write(JSON.stringify(denyOutput(
97
+ `[Article IV] ${result.reason}. Use environment variables or secure vaults instead of reading sensitive files directly.`
98
+ )));
100
99
  return;
101
100
  }
102
101
 
103
- process.stdout.write(JSON.stringify({ decision: 'allow' }));
102
+ process.stdout.write(JSON.stringify(allowOutput()));
104
103
  } catch (err) {
105
104
  process.stderr.write(`[chati-hook-error] read-protection: ${err?.message || 'unknown'}\n`);
106
- process.stdout.write(JSON.stringify({ decision: 'block', reason: 'Hook error fail-closed for safety' }));
105
+ // Fail-open: a broken hook should not prevent reads. Real protection
106
+ // patterns are explicit above and run before this catch.
107
+ process.stdout.write(JSON.stringify(allowOutput()));
107
108
  }
108
109
  }
109
110
 
111
+ function allowOutput() {
112
+ return {
113
+ hookSpecificOutput: {
114
+ hookEventName: 'PreToolUse',
115
+ permissionDecision: 'allow',
116
+ },
117
+ };
118
+ }
119
+
120
+ function denyOutput(reason) {
121
+ return {
122
+ hookSpecificOutput: {
123
+ hookEventName: 'PreToolUse',
124
+ permissionDecision: 'deny',
125
+ permissionDecisionReason: reason,
126
+ },
127
+ };
128
+ }
129
+
110
130
  export { isSensitivePath, SENSITIVE_PATTERNS, ALLOWED_EXCEPTIONS };
111
131
 
112
- // Only run main when executed directly (not imported by tests)
132
+ // Only run main when executed directly (not imported by tests).
133
+ // realpathSync resolves symlinks on both sides — important on macOS where
134
+ // /tmp is a symlink to /private/tmp. Without realpath the comparison fails
135
+ // silently and main() never runs.
113
136
  import { fileURLToPath } from 'url';
114
- if (process.argv[1] === fileURLToPath(import.meta.url)) {
115
- main();
137
+ import { realpathSync } from 'fs';
138
+ if (process.argv[1]) {
139
+ try {
140
+ if (realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url))) {
141
+ main();
142
+ }
143
+ } catch { /* path resolution failed — not invoked as a script */ }
116
144
  }