chati-dev 4.2.2 → 4.3.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 (221) hide show
  1. package/README.md +80 -53
  2. package/bin/chati.js +170 -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 +158 -22
  6. package/framework/agents/discover/brownfield-wu.md +26 -14
  7. package/framework/agents/discover/greenfield-wu.md +102 -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 +38 -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 +448 -0
  21. package/framework/agents/shared/visualizer.md +128 -0
  22. package/framework/config.yaml +7 -6
  23. package/framework/constitution.md +127 -44
  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 +24 -5
  35. package/framework/hooks/license-guard.js +62 -27
  36. package/framework/hooks/mode-governance.js +13 -2
  37. package/framework/hooks/model-governance.js +1 -0
  38. package/framework/hooks/post-dev.js +207 -0
  39. package/framework/hooks/prism-engine.js +274 -105
  40. package/framework/hooks/reasoning-escalator.js +371 -0
  41. package/framework/hooks/reference-trigger.js +117 -0
  42. package/framework/hooks/session-digest.js +50 -1
  43. package/framework/hooks/settings.json +32 -1
  44. package/framework/hooks/style-guard.js +25 -6
  45. package/framework/hooks/team-quality-gate.js +19 -12
  46. package/framework/hooks/undercover-guard.js +4 -2
  47. package/framework/i18n/en.yaml +3 -3
  48. package/framework/i18n/es.yaml +3 -3
  49. package/framework/i18n/fr.yaml +3 -3
  50. package/framework/i18n/pt.yaml +3 -3
  51. package/framework/intelligence/context-engine.md +4 -5
  52. package/framework/intelligence/decision-engine.md +1 -1
  53. package/framework/intelligence/hooks-performance.md +3 -3
  54. package/framework/migrations/v1.0-to-v1.1.yaml +1 -1
  55. package/framework/migrations/v1.4-to-v2.0.yaml +11 -11
  56. package/framework/migrations/v4.0-to-v4.1.yaml +2 -2
  57. package/framework/migrations/v4.2-to-v4.3.yaml +29 -0
  58. package/framework/orchestrator/chati-router.js +441 -0
  59. package/framework/orchestrator/chati-update.md +40 -40
  60. package/framework/orchestrator/chati.md +295 -94
  61. package/framework/rules/human-writing-style.md +47 -0
  62. package/framework/scaffold/motion-premium/README.md +99 -0
  63. package/framework/scaffold/motion-premium/app/globals.css.template +400 -0
  64. package/framework/scaffold/motion-premium/app/layout.tsx.template +110 -0
  65. package/framework/scaffold/motion-premium/components/animation/BackgroundCrossfadeProvider.tsx.template +170 -0
  66. package/framework/scaffold/motion-premium/components/animation/LenisProvider.tsx.template +49 -0
  67. package/framework/scaffold/motion-premium/components/animation/PageTransitionWrapper.tsx.template +83 -0
  68. package/framework/scaffold/motion-premium/components/animation/Preloader.tsx.template +171 -0
  69. package/framework/scaffold/motion-premium/components/ui/Container.tsx.template +69 -0
  70. package/framework/scaffold/motion-premium/components/ui/PageSection.tsx.template +74 -0
  71. package/framework/scaffold/motion-premium/lib/animations/gsap.ts.template +112 -0
  72. package/framework/scaffold/motion-premium/lib/animations/refreshCoordinator.ts.template +75 -0
  73. package/framework/scaffold/motion-premium/lib/animations/tokens.ts.template +119 -0
  74. package/framework/scaffold/motion-premium/lib/animations/useGsapContext.ts.template +92 -0
  75. package/framework/scaffold/motion-premium/lib/animations/useScrollSnapStepper.ts.template +265 -0
  76. package/framework/scaffold/motion-premium/lib/animations/useSmoothScroll.ts.template +67 -0
  77. package/framework/scaffold/motion-premium/lib/brand.ts.template +43 -0
  78. package/framework/scaffold/motion-premium/scaffold.yaml +178 -0
  79. package/framework/scaffold/motion-premium-3d/README.md +90 -0
  80. package/framework/scaffold/motion-premium-3d/app/(3d)/scroll-demo/ScrollDemoCanvas.tsx.template +81 -0
  81. package/framework/scaffold/motion-premium-3d/app/(3d)/scroll-demo/ScrollDemoClient.tsx.template +75 -0
  82. package/framework/scaffold/motion-premium-3d/app/(3d)/scroll-demo/page.tsx.template +26 -0
  83. package/framework/scaffold/motion-premium-3d/components/3d/CameraRig.tsx.template +100 -0
  84. package/framework/scaffold/motion-premium-3d/components/3d/CanvasProvider.tsx.template +85 -0
  85. package/framework/scaffold/motion-premium-3d/components/3d/InvalidateOnScroll.tsx.template +51 -0
  86. package/framework/scaffold/motion-premium-3d/components/3d/MeshCrossfade.tsx.template +79 -0
  87. package/framework/scaffold/motion-premium-3d/components/3d/ScrollCrossfade.tsx.template +88 -0
  88. package/framework/scaffold/motion-premium-3d/components/3d/ScrollScene.tsx.template +121 -0
  89. package/framework/scaffold/motion-premium-3d/components/webgl/SceneFallback.tsx.template +65 -0
  90. package/framework/scaffold/motion-premium-3d/components/webgl/WebGLContext.tsx.template +68 -0
  91. package/framework/scaffold/motion-premium-3d/lib/webgl/detect.ts.template +69 -0
  92. package/framework/scaffold/motion-premium-3d/scaffold.yaml +136 -0
  93. package/framework/schemas/session.schema.json +109 -21
  94. package/framework/scripts/reference-capture.js +430 -0
  95. package/framework/scripts/visual-qa.js +686 -0
  96. package/framework/tasks/orchestrator-handoff.md +1 -1
  97. package/framework/tasks/orchestrator-resume.md +1 -1
  98. package/framework/tasks/orchestrator-route.md +1 -1
  99. package/framework/tasks/orchestrator-status.md +3 -3
  100. package/framework/tasks/qa-planning-gate-define.md +1 -1
  101. package/framework/templates/brandbook-html-tmpl.md +2 -2
  102. package/framework/templates/brandbook-tmpl.yaml +1 -1
  103. package/framework/templates/component-spec-tmpl.yaml +1 -1
  104. package/framework/templates/design-token-tmpl.yaml +1 -1
  105. package/framework/templates/icon-system-tmpl.yaml +1 -1
  106. package/framework/templates/team-planning-tasks.yaml +6 -5
  107. package/framework/workflows/brownfield-discovery.yaml +2 -2
  108. package/framework/workflows/brownfield-fullstack.yaml +15 -11
  109. package/framework/workflows/brownfield-service.yaml +14 -10
  110. package/framework/workflows/brownfield-ui.yaml +15 -11
  111. package/framework/workflows/greenfield-fullstack.yaml +16 -13
  112. package/framework/workflows/quick-flow.yaml +3 -3
  113. package/framework/workflows/standard-flow.yaml +12 -9
  114. package/package.json +12 -6
  115. package/src/autonomy/autonomous-gate.js +1 -0
  116. package/src/autonomy/build-state.js +1 -2
  117. package/src/autonomy/progress-reporter.js +1 -1
  118. package/src/config/agent-customizer.js +11 -3
  119. package/src/config/claude-settings-generator.js +27 -7
  120. package/src/config/context-file-generator.js +41 -21
  121. package/src/config/framework-adapter.js +1 -0
  122. package/src/config/gemini-hooks-generator.js +19 -7
  123. package/src/config/mcp-configs.js +1 -0
  124. package/src/context/layers/l1-global.js +2 -1
  125. package/src/dashboard/data-reader.js +4 -3
  126. package/src/dashboard/layout.js +2 -1
  127. package/src/decision/analyzer.js +6 -30
  128. package/src/decision/engine.js +4 -28
  129. package/src/decision/registry-healer.js +3 -2
  130. package/src/decision/registry-updater.js +23 -14
  131. package/src/extensions/loader.js +2 -8
  132. package/src/gates/g1-planning-complete.js +2 -1
  133. package/src/gates/g2-qa-planning.js +2 -1
  134. package/src/gates/g3-implementation.js +2 -1
  135. package/src/gates/g4-qa-implementation.js +3 -2
  136. package/src/gates/g5-deploy-ready.js +2 -1
  137. package/src/health/engine.js +4 -3
  138. package/src/installer/core.js +431 -81
  139. package/src/installer/preflight.js +131 -0
  140. package/src/installer/provider-overlay.js +3 -3
  141. package/src/installer/scaffold-applier.js +358 -0
  142. package/src/installer/templates.js +46 -29
  143. package/src/installer/validator.js +17 -12
  144. package/src/intelligence/registry-manager.js +22 -29
  145. package/src/intelligence/timeline.js +11 -6
  146. package/src/license/commands.js +1 -1
  147. package/src/license/wait.js +102 -0
  148. package/src/memory/agent-memory.js +81 -0
  149. package/src/memory/dream.js +32 -1
  150. package/src/merger/replace-merger.js +28 -15
  151. package/src/orchestrator/agent-selector.js +2 -1
  152. package/src/orchestrator/cli.js +1880 -71
  153. package/src/orchestrator/doctor.js +270 -0
  154. package/src/orchestrator/handoff-engine.js +4 -3
  155. package/src/orchestrator/index.js +2 -0
  156. package/src/orchestrator/pipeline-manager.js +306 -15
  157. package/src/orchestrator/session-manager.js +339 -6
  158. package/src/tasks/handoff.js +3 -2
  159. package/src/telemetry/config.js +4 -3
  160. package/src/telemetry/schema.js +1 -0
  161. package/src/terminal/collector.js +3 -2
  162. package/src/terminal/index.js +1 -2
  163. package/src/terminal/isolation.js +52 -18
  164. package/src/terminal/prompt-builder.js +42 -25
  165. package/src/terminal/run-parallel.js +1 -1
  166. package/src/terminal/run-team.js +3 -3
  167. package/src/terminal/team-task-list.js +43 -4
  168. package/src/upgrade/backup.js +3 -2
  169. package/src/upgrade/checker.js +3 -2
  170. package/src/upgrade/migrator.js +65 -7
  171. package/src/upgrade/tracked-files-detector.js +86 -0
  172. package/src/upgrade/user-messages.js +94 -0
  173. package/src/utils/config-parser.js +2 -1
  174. package/src/utils/feature-flags.js +2 -1
  175. package/src/utils/flatten-entities.js +69 -0
  176. package/src/utils/framework-dir.js +16 -0
  177. package/src/utils/model-id.js +85 -0
  178. package/src/utils/provider-limits.js +84 -23
  179. package/src/utils/schema-validator.js +1 -1
  180. package/src/wizard/i18n.js +5 -4
  181. package/src/wizard/index.js +14 -0
  182. package/assets/logo - c/303/263pia.png +0 -0
  183. package/assets/logo.svg +0 -42
  184. package/assets/logo2.png +0 -0
  185. package/assets/social-preview.png +0 -0
  186. package/scripts/bundle-framework.js +0 -69
  187. package/scripts/changelog-generator.js +0 -222
  188. package/scripts/codebase-mapper.js +0 -728
  189. package/scripts/commit-message-generator.js +0 -167
  190. package/scripts/coverage-analyzer.js +0 -260
  191. package/scripts/dependency-analyzer.js +0 -280
  192. package/scripts/doctor/checks/agents.js +0 -77
  193. package/scripts/doctor/checks/constitution.js +0 -41
  194. package/scripts/doctor/checks/domain-alignment.js +0 -58
  195. package/scripts/doctor/checks/prism-layers.js +0 -84
  196. package/scripts/doctor/checks/registry.js +0 -55
  197. package/scripts/doctor/checks/schemas.js +0 -61
  198. package/scripts/doctor/fixes/reference-fix.js +0 -100
  199. package/scripts/doctor/fixes/registry-fix.js +0 -56
  200. package/scripts/doctor/index.js +0 -212
  201. package/scripts/framework-analyzer.js +0 -308
  202. package/scripts/generate-constitution-domain.js +0 -253
  203. package/scripts/generate-signing-key.js +0 -33
  204. package/scripts/health-check.js +0 -481
  205. package/scripts/ide-sync.js +0 -326
  206. package/scripts/performance-analyzer.js +0 -325
  207. package/scripts/plan-tracker.js +0 -278
  208. package/scripts/populate-entity-registry.js +0 -481
  209. package/scripts/pr-review.js +0 -317
  210. package/scripts/rollback-manager.js +0 -310
  211. package/scripts/semantic-lint.js +0 -328
  212. package/scripts/sign-manifest.js +0 -53
  213. package/scripts/stuck-detector.js +0 -343
  214. package/scripts/test-quality-assessment.js +0 -257
  215. package/scripts/validate-agents.js +0 -368
  216. package/scripts/validate-package.js +0 -505
  217. package/scripts/validate-tasks.js +0 -465
  218. package/src/autonomy/worktree-manager.js +0 -250
  219. package/src/intelligence/decision-engine.js +0 -256
  220. package/src/intelligence/document-sharder.js +0 -221
  221. 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 (empty output = allow)
69
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);
283
+
284
+ // 1. Model + window detection.
285
+ const detectedModel = detectModel(event, session);
286
+ const contextLimit = resolveLimit(limits, detectedModel, session.provider);
94
287
 
95
- // Primary: turn-count tracks cumulative context consumption across the session.
96
- // Fallback: prompt-length for first turn or missing session data.
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));
293
+
294
+ // 3. Bracket computation (percentage-based, model-scaled).
295
+ const bracket = computeBracket(remainingPercent);
107
296
 
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';
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
+ }
113
309
 
114
- // Load agent memory (if active agent has MEMORY.md)
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>` : '',
@@ -163,9 +354,6 @@ async function main() {
163
354
  '</chati-context>',
164
355
  ].filter(Boolean).join('\n');
165
356
 
166
- // UserPromptSubmit context injection schema:
167
- // hookSpecificOutput.additionalContext is the canonical field for
168
- // injecting text into Claude's view of the current prompt.
169
357
  process.stdout.write(JSON.stringify({
170
358
  hookSpecificOutput: {
171
359
  hookEventName: 'UserPromptSubmit',
@@ -174,43 +362,24 @@ async function main() {
174
362
  }));
175
363
  } catch (err) {
176
364
  process.stderr.write(`[chati] prism-engine: ${err.message}\n`);
177
- // Silent allow on error — empty object = no injection, no block.
178
365
  process.stdout.write('{}');
179
366
  }
180
367
  }
181
368
 
182
- /**
183
- * Frustration detection patterns.
184
- * Inspired by Claude Code's regex-based emotion detection.
185
- * Soft advisory only — never blocks or overrides behavior.
186
- */
187
- const FRUSTRATION_PATTERNS = [
188
- /this (is|isn't|isnt) working/i,
189
- /I (already|just) (told|said|asked)/i,
190
- /why (won't|doesn't|can't|isn't|wont|doesnt|cant|isnt)/i,
191
- /stop (doing|repeating|ignoring)/i,
192
- /you('re| are) (not|never) (listening|reading|understanding)/i,
193
- /for the (second|third|fourth|last) time/i,
194
- /!!+/,
195
- /\b(WTF|WHAT THE|FFS|OMG)\b/i,
196
- ];
197
-
198
- /**
199
- * Detect user frustration from prompt text.
200
- * @param {string} prompt - User prompt text
201
- * @returns {boolean} True if frustration patterns detected
202
- */
203
- function detectFrustration(prompt) {
204
- if (!prompt || prompt.length < 5) return false;
205
- return FRUSTRATION_PATTERNS.some(p => p.test(prompt));
206
- }
207
-
208
- export { readSessionState, detectFrustration };
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
+ };
209
381
 
210
382
  // Only run main when executed directly (not imported by tests).
211
- // realpathSync resolves symlinks on both sides — important on macOS where
212
- // /tmp is a symlink to /private/tmp. Without realpath the comparison fails
213
- // silently and main() never runs.
214
383
  import { fileURLToPath } from 'url';
215
384
  import { realpathSync } from 'fs';
216
385
  if (process.argv[1]) {