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
@@ -0,0 +1,371 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Reasoning Escalator Hook — Article XXIII enforcement.
4
+ *
5
+ * Two entry points via event-type branching:
6
+ *
7
+ * UserPromptSubmit:
8
+ * Read the user message, evaluate escalation triggers, update
9
+ * session.reasoning_tier. Non-blocking — always allows the prompt
10
+ * through. Side effect only.
11
+ *
12
+ * PreToolUse (matcher Write|Edit|NotebookEdit):
13
+ * If current tier is 'deliberate' AND the target is a source file
14
+ * AND no recent plan exists in .planning/, DENY the tool call with
15
+ * a message telling the agent to write a plan first. Otherwise allow.
16
+ *
17
+ * Pure evaluation logic is exported for unit tests. The main() entry
18
+ * point handles stdin/stdout and session persistence (regex-based YAML
19
+ * mutation — no js-yaml dependency).
20
+ */
21
+
22
+ import { existsSync, readFileSync, writeFileSync, readdirSync, statSync } from 'fs';
23
+ import { join, isAbsolute, relative } from 'path';
24
+ import { execSync } from 'child_process';
25
+
26
+ // ---------------------------------------------------------------------------
27
+ // Pure helpers (exported for testing)
28
+ // ---------------------------------------------------------------------------
29
+
30
+ export const TIERS = ['standard', 'deep', 'deliberate'];
31
+ export const TIER_RANK = { standard: 0, deep: 1, deliberate: 2 };
32
+
33
+ // English-canonical friction keywords — always active regardless of
34
+ // session.language. Article VII mandates English for all framework source
35
+ // material; per-language patterns live in FRICTION_KEYWORDS_LOCALES and are
36
+ // merged on top when session.language selects them.
37
+ export const FRICTION_KEYWORDS_EN = [
38
+ /\bredesign\b/i,
39
+ /\brefactor(?:ing|s)?\b/i,
40
+ /\bstuck\b/i,
41
+ /\b(?:in a |a )?loop\b/i,
42
+ /\bnot working\b/i,
43
+ /\bbreak(?:ing|s)?\b/i,
44
+ ];
45
+
46
+ // Locale-specific patterns merged on top of EN when session.language matches.
47
+ // Each array MUST cover only that locale's phrasings of the same friction
48
+ // concepts (refactoring / not working / stuck). Adding a locale does not
49
+ // mutate the EN set.
50
+ export const FRICTION_KEYWORDS_LOCALES = {
51
+ pt: [
52
+ /\brefatora[çc][aã]o\b/i,
53
+ /\bn[aã]o funciona\b/i,
54
+ /\btravad[oa]\b/i,
55
+ ],
56
+ es: [
57
+ /\brefactorizaci[oó]n\b/i,
58
+ /\bno funciona\b/i,
59
+ /\batascad[oa]\b/i,
60
+ ],
61
+ fr: [
62
+ /\brefactorisation\b/i,
63
+ /\bne marche pas\b/i,
64
+ /\bbloqu[eé]\b/i,
65
+ ],
66
+ };
67
+
68
+ /**
69
+ * Build the active friction-keyword set for a given language.
70
+ * EN is always included; the locale layer is appended if recognised.
71
+ *
72
+ * @param {string} [language='en'] ISO 639-1 language code from session.language
73
+ * @returns {RegExp[]}
74
+ */
75
+ export function getFrictionKeywords(language = 'en') {
76
+ const locale = FRICTION_KEYWORDS_LOCALES[language] ?? [];
77
+ return [...FRICTION_KEYWORDS_EN, ...locale];
78
+ }
79
+
80
+ // Backwards-compat default export: English-only set. Callers that care about
81
+ // locale coverage should use getFrictionKeywords(session.language) instead.
82
+ export const FRICTION_KEYWORDS = getFrictionKeywords('en');
83
+
84
+ export const SOURCE_FILE_EXT = /\.(?:js|jsx|ts|tsx|mjs|cjs|py|rb|go|rs|java|cs|php|swift|kt|scala|c|cc|cpp|h|hpp|css|scss|sass|html|vue|svelte|astro|sql)$/i;
85
+
86
+ /**
87
+ * Parse slash-command style explicit tier commands from a user message.
88
+ * Returns one of 'ultraplan' | 'deliberate' | 'quick' | null.
89
+ */
90
+ export function detectExplicitCommand(msg) {
91
+ if (!msg || typeof msg !== 'string') return null;
92
+ const m = msg.match(/(^|\s)\/(ultraplan|deliberate|quick)\b/i);
93
+ return m ? m[2].toLowerCase() : null;
94
+ }
95
+
96
+ /**
97
+ * Pure tier-escalation evaluator. All inputs are explicit so the function
98
+ * is fully deterministic and testable.
99
+ *
100
+ * @param {object} input
101
+ * @param {'standard'|'deep'|'deliberate'} [input.currentTier]
102
+ * @param {number} [input.correctionCycles]
103
+ * @param {number} [input.decisionTrailOpen]
104
+ * @param {number} [input.filesModifiedCount]
105
+ * @param {number} [input.linesModified]
106
+ * @param {string} [input.userMessage]
107
+ * @param {boolean} [input.crossViewportRegression]
108
+ * @param {boolean} [input.scopeEscalation]
109
+ * @param {string} [input.language='en'] ISO 639-1 session language; selects the
110
+ * locale-specific friction-keyword layer on top of the English canonical set.
111
+ * @returns {{ tier: string, triggers: string[], reason: string }}
112
+ */
113
+ export function evaluateTier(input = {}) {
114
+ const {
115
+ currentTier = 'standard',
116
+ correctionCycles = 0,
117
+ decisionTrailOpen = 0,
118
+ filesModifiedCount = 0,
119
+ linesModified = 0,
120
+ userMessage = '',
121
+ crossViewportRegression = false,
122
+ scopeEscalation = false,
123
+ language = 'en',
124
+ } = input;
125
+
126
+ // Explicit user command always wins.
127
+ const explicit = detectExplicitCommand(userMessage);
128
+ if (explicit === 'quick') {
129
+ return { tier: 'standard', triggers: ['user:/quick'], reason: 'User requested quick mode; tier forced to standard.' };
130
+ }
131
+ if (explicit === 'ultraplan' || explicit === 'deliberate') {
132
+ return { tier: 'deliberate', triggers: [`user:/${explicit}`], reason: 'User requested deliberate mode.' };
133
+ }
134
+
135
+ const triggers = [];
136
+ let targetTier = 'standard';
137
+ const promote = (toTier, label) => {
138
+ triggers.push(label);
139
+ if (TIER_RANK[toTier] > TIER_RANK[targetTier]) targetTier = toTier;
140
+ };
141
+
142
+ if (correctionCycles >= 2) promote('deliberate', `correction_cycles=${correctionCycles}`);
143
+ if (scopeEscalation) promote('deliberate', 'scope_escalation');
144
+ if (filesModifiedCount > 5) promote('deep', `files_modified=${filesModifiedCount}`);
145
+ if (linesModified > 200) promote('deep', `lines_modified=${linesModified}`);
146
+ if (decisionTrailOpen > 2) promote('deep', `decision_trail_open=${decisionTrailOpen}`);
147
+ if (crossViewportRegression) promote('deep', 'cross_viewport_regression');
148
+ const frictionSet = getFrictionKeywords(language);
149
+ if (userMessage && frictionSet.some(r => r.test(userMessage))) {
150
+ const matched = frictionSet.filter(r => r.test(userMessage)).length;
151
+ promote('deep', `friction_keywords=${matched}`);
152
+ }
153
+
154
+ // Monotonic: never auto-demote within a task.
155
+ const finalTier = TIER_RANK[targetTier] > TIER_RANK[currentTier] ? targetTier : currentTier;
156
+ const reason = triggers.length === 0
157
+ ? `No escalation signals. Keeping tier "${finalTier}".`
158
+ : `Triggers (${triggers.length}): ${triggers.join(', ')}`;
159
+ return { tier: finalTier, triggers, reason };
160
+ }
161
+
162
+ /**
163
+ * Check whether a deliberate-tier tool call must be blocked pending a plan.
164
+ *
165
+ * @param {object} input
166
+ * @param {string} input.tier
167
+ * @param {string} input.toolName
168
+ * @param {object} input.toolInput
169
+ * @param {string} input.projectDir
170
+ * @param {number} [input.sessionStartedAt] - ms epoch; plans older than this do not count
171
+ * @returns {{ block: boolean, reason?: string, message?: string }}
172
+ */
173
+ export function checkDeliberatePlanRequirement(input) {
174
+ const { tier, toolName, toolInput, projectDir, sessionStartedAt = 0 } = input || {};
175
+ if (tier !== 'deliberate') return { block: false };
176
+ if (!['Edit', 'Write', 'NotebookEdit'].includes(toolName)) return { block: false };
177
+
178
+ const rawTarget = (toolInput && (toolInput.file_path || toolInput.path)) || '';
179
+ if (!rawTarget) return { block: false };
180
+
181
+ const target = String(rawTarget);
182
+
183
+ // Writing TO a plan file is permitted (that's how the agent satisfies the gate).
184
+ if (/(^|\/)\.planning\//.test(target)) return { block: false };
185
+ // Writing to non-source files (configs, metadata) is permitted.
186
+ if (!SOURCE_FILE_EXT.test(target)) return { block: false };
187
+
188
+ const planningDir = projectDir ? join(projectDir, '.planning') : null;
189
+ if (!planningDir || !existsSync(planningDir)) {
190
+ return {
191
+ block: true,
192
+ reason: 'deliberate_tier_requires_plan',
193
+ message: 'Reasoning tier is DELIBERATE (Article XXIII). Create .planning/<task>-plan.md with approach, alternatives, risks, and acceptance criteria BEFORE editing code.',
194
+ };
195
+ }
196
+
197
+ let entries = [];
198
+ try { entries = readdirSync(planningDir); } catch { /* ignore */ }
199
+ const hasRecentPlan = entries.some(f => {
200
+ if (!f.endsWith('.md')) return false;
201
+ try {
202
+ const s = statSync(join(planningDir, f));
203
+ return s.mtimeMs >= sessionStartedAt;
204
+ } catch { return false; }
205
+ });
206
+ if (hasRecentPlan) return { block: false };
207
+
208
+ return {
209
+ block: true,
210
+ reason: 'deliberate_tier_requires_plan',
211
+ message: 'Reasoning tier is DELIBERATE (Article XXIII). No plan file in .planning/ newer than the session start. Write the plan before editing code.',
212
+ };
213
+ }
214
+
215
+ // ---------------------------------------------------------------------------
216
+ // I/O wrappers (not exercised in unit tests; covered by integration)
217
+ // ---------------------------------------------------------------------------
218
+
219
+ /**
220
+ * Parse the reasoning_tier and started_at fields out of session.yaml using
221
+ * regex. Stays dependency-free (hooks do not import js-yaml).
222
+ */
223
+ export function readSessionTier(projectDir) {
224
+ const sessionPath = join(projectDir, '.chati', 'session.yaml');
225
+ if (!existsSync(sessionPath)) return { tier: 'standard', startedAt: 0, raw: null };
226
+ let raw = '';
227
+ try { raw = readFileSync(sessionPath, 'utf-8'); } catch { return { tier: 'standard', startedAt: 0, raw: null }; }
228
+ const tierMatch = raw.match(/^\s*reasoning_tier:\s*(["']?)(\w+)\1\s*$/m);
229
+ const startedMatch = raw.match(/^\s*started_at:\s*(["']?)([^"'\n]+)\1\s*$/m);
230
+ let startedAt = 0;
231
+ if (startedMatch) {
232
+ const t = Date.parse(startedMatch[2]);
233
+ if (!Number.isNaN(t)) startedAt = t;
234
+ }
235
+ const tier = tierMatch ? tierMatch[2] : 'standard';
236
+ return { tier: TIERS.includes(tier) ? tier : 'standard', startedAt, raw };
237
+ }
238
+
239
+ /**
240
+ * Parse session.language out of session.yaml. Defaults to 'en' if the key is
241
+ * missing or unreadable. Kept regex-based to avoid a js-yaml dep in hooks.
242
+ */
243
+ export function readSessionLanguage(projectDir) {
244
+ const sessionPath = join(projectDir, '.chati', 'session.yaml');
245
+ if (!existsSync(sessionPath)) return 'en';
246
+ let raw = '';
247
+ try { raw = readFileSync(sessionPath, 'utf-8'); } catch { return 'en'; }
248
+ const m = raw.match(/^\s*language:\s*(["']?)([a-z]{2})\1\s*$/m);
249
+ return m ? m[2] : 'en';
250
+ }
251
+
252
+ /**
253
+ * Update session.yaml with a new reasoning_tier value. Append `reasoning_tier:`
254
+ * if missing; replace in place if present. Silent on failure.
255
+ */
256
+ export function writeSessionTier(projectDir, tier) {
257
+ if (!TIERS.includes(tier)) return false;
258
+ const sessionPath = join(projectDir, '.chati', 'session.yaml');
259
+ if (!existsSync(sessionPath)) return false;
260
+ let raw = '';
261
+ try { raw = readFileSync(sessionPath, 'utf-8'); } catch { return false; }
262
+ const line = `reasoning_tier: ${tier}`;
263
+ let next;
264
+ if (/^\s*reasoning_tier:\s*\w+\s*$/m.test(raw)) {
265
+ next = raw.replace(/^\s*reasoning_tier:\s*\w+\s*$/m, line);
266
+ } else {
267
+ next = raw.trimEnd() + '\n' + line + '\n';
268
+ }
269
+ try { writeFileSync(sessionPath, next, 'utf-8'); return true; } catch { return false; }
270
+ }
271
+
272
+ /**
273
+ * Count files modified since HEAD via git. Returns -1 when git is unavailable.
274
+ */
275
+ export function gitDiffStats(projectDir) {
276
+ if (!projectDir) return { files: -1, lines: -1 };
277
+ try {
278
+ const files = execSync('git diff --name-only HEAD 2>/dev/null', {
279
+ cwd: projectDir, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'],
280
+ }).trim().split('\n').filter(Boolean);
281
+ let lines = 0;
282
+ try {
283
+ const stat = execSync('git diff --shortstat HEAD 2>/dev/null', {
284
+ cwd: projectDir, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'],
285
+ }).trim();
286
+ const m = stat.match(/(\d+)\s+insertion.*?(\d+)\s+deletion/);
287
+ if (m) lines = parseInt(m[1], 10) + parseInt(m[2], 10);
288
+ else {
289
+ const ins = stat.match(/(\d+)\s+insertion/);
290
+ if (ins) lines = parseInt(ins[1], 10);
291
+ }
292
+ } catch { /* ignore */ }
293
+ return { files: files.length, lines };
294
+ } catch {
295
+ return { files: -1, lines: -1 };
296
+ }
297
+ }
298
+
299
+ function allowOutput() {
300
+ return { hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'allow' } };
301
+ }
302
+ function denyOutput(reason) {
303
+ return {
304
+ hookSpecificOutput: {
305
+ hookEventName: 'PreToolUse',
306
+ permissionDecision: 'deny',
307
+ permissionDecisionReason: reason,
308
+ },
309
+ };
310
+ }
311
+ function userPromptPassthrough() {
312
+ // UserPromptSubmit hooks emit a different shape; nothing to block here.
313
+ return {};
314
+ }
315
+
316
+ async function main() {
317
+ let input = '';
318
+ for await (const chunk of process.stdin) input += chunk;
319
+
320
+ try {
321
+ const event = JSON.parse(input || '{}');
322
+ const projectDir = event.cwd || process.cwd();
323
+ const hookEventName = event.hook_event_name || event.hookEventName || '';
324
+
325
+ if (hookEventName === 'UserPromptSubmit') {
326
+ const userMessage = event.user_message || event.prompt || '';
327
+ const { tier: currentTier, startedAt: _s } = readSessionTier(projectDir);
328
+ const language = readSessionLanguage(projectDir);
329
+ const { files: filesModifiedCount, lines: linesModified } = gitDiffStats(projectDir);
330
+ const result = evaluateTier({
331
+ currentTier,
332
+ userMessage,
333
+ filesModifiedCount: Math.max(0, filesModifiedCount),
334
+ linesModified: Math.max(0, linesModified),
335
+ language,
336
+ });
337
+ if (result.tier !== currentTier) {
338
+ writeSessionTier(projectDir, result.tier);
339
+ }
340
+ process.stdout.write(JSON.stringify(userPromptPassthrough()));
341
+ return;
342
+ }
343
+
344
+ if (hookEventName === 'PreToolUse') {
345
+ const toolName = event.tool_name || '';
346
+ const toolInput = event.tool_input || {};
347
+ const { tier, startedAt } = readSessionTier(projectDir);
348
+ const verdict = checkDeliberatePlanRequirement({
349
+ tier, toolName, toolInput, projectDir, sessionStartedAt: startedAt,
350
+ });
351
+ if (verdict.block) {
352
+ process.stdout.write(JSON.stringify(denyOutput(`[Article XXIII] ${verdict.message}`)));
353
+ } else {
354
+ process.stdout.write(JSON.stringify(allowOutput()));
355
+ }
356
+ return;
357
+ }
358
+
359
+ // Unknown event — pass through.
360
+ process.stdout.write(JSON.stringify(allowOutput()));
361
+ } catch (err) {
362
+ // Fail-OPEN: hook errors must not block user work.
363
+ process.stderr.write(`[chati-hook-error] reasoning-escalator: ${err?.message || 'unknown'}\n`);
364
+ process.stdout.write(JSON.stringify(allowOutput()));
365
+ }
366
+ }
367
+
368
+ // Only run main() when invoked as a script (tests can import pure helpers).
369
+ if (import.meta.url === `file://${process.argv[1] || ''}`) {
370
+ main();
371
+ }
@@ -0,0 +1,117 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Reference Trigger Hook — PostToolUse (runs after Write tool)
4
+ *
5
+ * When the Brief agent writes brief-report.md, this hook:
6
+ * 1. Extracts visual_references URLs
7
+ * 2. Launches reference-capture.js in BACKGROUND (detached)
8
+ * 3. Appends a "Visual Reference Captures" note to brief-report.md
9
+ *
10
+ * The capture runs in background so Claude doesn't wait ~9 min.
11
+ * The script writes capture-complete.json when ALL sites finish.
12
+ * Brand Architect polls for this file before spawning Visualizer.
13
+ */
14
+
15
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
16
+ import { join } from 'path';
17
+ import { spawn } from 'child_process';
18
+
19
+ async function readStdin() {
20
+ let data = '';
21
+ for await (const chunk of process.stdin) data += chunk;
22
+ return data;
23
+ }
24
+
25
+ async function main() {
26
+ let input;
27
+ try {
28
+ input = JSON.parse(await readStdin() || '{}');
29
+ } catch {
30
+ process.stdout.write('{}\n');
31
+ return;
32
+ }
33
+
34
+ const toolName = input.tool_name || '';
35
+ const toolInput = input.tool_input || {};
36
+ const filePath = toolInput.file_path || '';
37
+ const cwd = input.cwd || process.cwd();
38
+
39
+ process.stdout.write('{}\n');
40
+
41
+ if (toolName !== 'Write') return;
42
+ if (!filePath.endsWith('1-Brief/brief-report.md')) return;
43
+ if (!existsSync(filePath)) return;
44
+
45
+ let content;
46
+ try { content = readFileSync(filePath, 'utf-8'); } catch { return; }
47
+
48
+ // Don't re-run if already triggered (idempotency)
49
+ if (content.includes('## Visual Reference Captures')) return;
50
+
51
+ // Extract URLs — support both `url: "https://..."` YAML format and bare `- https://...` list items
52
+ const urlPatterns = [
53
+ /url:\s*["']?(https?:\/\/[^\s"']+)["']?/g,
54
+ /^\s*-\s+(https?:\/\/[^\s"']+)/gm,
55
+ ];
56
+ const urlSet = new Set();
57
+ for (const pattern of urlPatterns) {
58
+ for (const match of content.matchAll(pattern)) {
59
+ urlSet.add(match[1]);
60
+ }
61
+ }
62
+ const urls = [...urlSet];
63
+ if (urls.length === 0) return;
64
+
65
+ // Find script + playwright
66
+ const fwCandidates = ['.chati.dev', 'chati.dev'];
67
+ let scriptPath = null;
68
+ let nodePath = process.env.NODE_PATH || '';
69
+ for (const fw of fwCandidates) {
70
+ const p = join(cwd, fw, 'scripts', 'reference-capture.js');
71
+ if (existsSync(p)) { scriptPath = p; break; }
72
+ }
73
+ if (!scriptPath) return;
74
+ for (const fw of fwCandidates) {
75
+ const cliModules = join(cwd, fw, '_cli', 'node_modules');
76
+ if (existsSync(join(cliModules, 'playwright'))) {
77
+ nodePath = nodePath ? `${cliModules}:${nodePath}` : cliModules;
78
+ break;
79
+ }
80
+ }
81
+
82
+ const refOutput = join(cwd, 'artifacts', '4-UX', 'references');
83
+ mkdirSync(refOutput, { recursive: true });
84
+
85
+ // Launch capture in BACKGROUND — don't block Claude.
86
+ // The script writes capture-complete.json when done.
87
+ const child = spawn('node', [scriptPath, '--urls', ...urls, '--output', refOutput], {
88
+ cwd,
89
+ detached: true,
90
+ stdio: 'ignore',
91
+ env: { ...process.env, NODE_PATH: nodePath },
92
+ });
93
+ child.unref();
94
+
95
+ // Append note to brief-report.md immediately (capture runs in background)
96
+ const captureSection = `\n\n## Visual Reference Captures
97
+
98
+ > Visual reference capture is running in background for ${urls.length} site(s).
99
+ > Screenshots, scroll videos, and CSS tokens are being captured via Playwright.
100
+ >
101
+ > **IMPORTANT for planning agents:** This section contains preliminary data.
102
+ > A deeper visual analysis of the scroll experience, animation patterns, and design DNA
103
+ > will be performed by the **UX Brand Architect** using the Visualizer agent during planning.
104
+ > Do NOT base final design decisions solely on this summary — wait for the full
105
+ > Visual Experience Analysis in \`artifacts/4-UX/reference-analysis.md\`.
106
+ >
107
+ > **Sites being captured:** ${urls.map(u => `\`${u}\``).join(', ')}
108
+ > **Output:** \`artifacts/4-UX/references/\`
109
+ > **Status:** Check \`artifacts/4-UX/references/capture-complete.json\` for completion status.
110
+ `;
111
+
112
+ try {
113
+ writeFileSync(filePath, content + captureSection, 'utf-8');
114
+ } catch { /* ignore */ }
115
+ }
116
+
117
+ main().catch(() => process.stdout.write('{}\n'));
@@ -96,6 +96,55 @@ async function main() {
96
96
 
97
97
  writeFileSync(join(digestDir, fileName), lines.join('\n') + '\n', 'utf-8');
98
98
 
99
+ // Memory-bootstrap-v1 (F-A5-006): when the current agent has no
100
+ // MEMORY.md yet (or it is empty), write a minimal one from this digest
101
+ // so prism-engine's L4 memory layer is never empty in early sessions.
102
+ // Inline here instead of importing from src/memory/agent-memory.js
103
+ // because hooks run via raw Node and cannot reliably import the
104
+ // package source on installed projects. The shape mirrors the
105
+ // writeBootstrapMemory() contract in agent-memory.js.
106
+ try {
107
+ const agent = digest.currentAgent;
108
+ if (agent && agent !== 'none') {
109
+ const memDir = join(projectDir, '.chati', 'memories', agent);
110
+ const memPath = join(memDir, 'MEMORY.md');
111
+ let isEmpty = true;
112
+ if (existsSync(memPath)) {
113
+ try { isEmpty = readFileSync(memPath, 'utf-8').trim().length === 0; }
114
+ catch { /* treat as empty */ }
115
+ }
116
+ if (isEmpty) {
117
+ const bootstrap = [];
118
+ bootstrap.push('<!-- bootstrap-write: true -->');
119
+ bootstrap.push('<!-- source: session-digest bootstrap (memory-bootstrap-v1) -->');
120
+ bootstrap.push('');
121
+ bootstrap.push('## Episodic');
122
+ bootstrap.push('');
123
+ bootstrap.push(`- Resumed at mode=${digest.mode}, pipeline_position=${digest.pipelinePosition}, workflow=${digest.workflow || 'unknown'}. (low) [source:bootstrap]`);
124
+ if (digest.active_task) {
125
+ bootstrap.push('');
126
+ bootstrap.push('## Semantic');
127
+ bootstrap.push('');
128
+ bootstrap.push(`- Active task on resume: ${String(digest.active_task).slice(0, 500)} (low) [source:bootstrap]`);
129
+ }
130
+ if (Array.isArray(digest.gotchas_found) && digest.gotchas_found.length > 0) {
131
+ bootstrap.push('');
132
+ bootstrap.push('## Procedural');
133
+ bootstrap.push('');
134
+ for (const g of digest.gotchas_found.slice(0, 5)) {
135
+ if (!g?.message) continue;
136
+ bootstrap.push(`- Known gotcha [${g.severity || 'info'}] ${g.id || ''}: ${String(g.message).slice(0, 200)} (low) [source:bootstrap, severity:${g.severity || 'info'}]`);
137
+ }
138
+ }
139
+ bootstrap.push('');
140
+ mkdirSync(memDir, { recursive: true });
141
+ writeFileSync(memPath, bootstrap.join('\n'), 'utf-8');
142
+ }
143
+ }
144
+ } catch (err) {
145
+ process.stderr.write(`[chati] session-digest bootstrap: ${err.message}\n`);
146
+ }
147
+
99
148
  // Auto daily digest: append session entry on every PreCompact
100
149
  try {
101
150
  const dailyDir = join(projectDir, '.chati', 'memories', 'shared', 'daily');
@@ -115,7 +164,9 @@ async function main() {
115
164
  process.stderr.write(`[chati] session-digest daily-append: ${err.message}\n`);
116
165
  }
117
166
 
118
- // Auto memory consolidation: trigger when memories accumulate
167
+ // Auto memory consolidation: trigger when memories accumulate.
168
+ // CRITICAL: must be fully async (detached spawn) — PreCompact runs on the
169
+ // hot path and execSync would add up to 10s of latency to every compact.
119
170
  try {
120
171
  const memBase = join(projectDir, '.chati', 'memories');
121
172
  if (existsSync(memBase)) {
@@ -132,16 +183,19 @@ async function main() {
132
183
  // Consolidate when > 100 entries (50% of 200 cap)
133
184
  if (totalEntries > 100) {
134
185
  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');
186
+ // Fire-and-forget detached spawn returns immediately, child outlives parent.
187
+ const { spawn } = await import('child_process');
138
188
  try {
139
- execSync(`node -e "import('./packages/chati-dev/src/memory/dream.js').then(m => m.runDreamConsolidation('${projectDir.replace(/'/g, "\\'")}'))"`, {
189
+ const child = spawn('node', [
190
+ '-e',
191
+ `import(${JSON.stringify(projectDir + '/node_modules/chati-dev/src/memory/dream.js')}).then(m => m.runDreamConsolidation(${JSON.stringify(projectDir)}))`,
192
+ ], {
140
193
  cwd: projectDir,
141
- timeout: 10000,
194
+ detached: true,
142
195
  stdio: 'ignore',
143
196
  });
144
- } catch { /* expected: consolidation may timeout or fail */ }
197
+ child.unref();
198
+ } catch { /* expected: spawn may fail in restricted env */ }
145
199
  }
146
200
  }
147
201
  } catch (err) {
@@ -149,8 +203,9 @@ async function main() {
149
203
  }
150
204
  }
151
205
 
152
- // Session telemetry ping (moved from license-guard to reduce per-prompt overhead)
153
- // 5-minute throttle with phase/agent change detection
206
+ // Session telemetry ping (owned by session-digest, was moved here from license-guard).
207
+ // 5-minute throttle with phase/agent change detection.
208
+ // Pings.yaml format: '/path/to/project': '<ISO8601>|<phase>|<agent>'
154
209
  try {
155
210
  const homedir = process.env.HOME || '';
156
211
  const pingsPath = join(homedir, '.chati-dev', 'pings.yaml');
@@ -163,15 +218,17 @@ async function main() {
163
218
 
164
219
  if (licenseKey && digest) {
165
220
  let shouldPing = false;
166
- const now = Date.now();
221
+ const nowIso = new Date().toISOString();
167
222
  const currentState = `${digest.mode}|${digest.currentAgent}`;
168
223
 
169
224
  if (existsSync(pingsPath)) {
170
225
  const pingsRaw = readFileSync(pingsPath, 'utf-8');
171
- const entryMatch = pingsRaw.match(new RegExp(`${projectDir.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}.*?'([^']+)'`));
226
+ const escaped = projectDir.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
227
+ const entryMatch = pingsRaw.match(new RegExp(`^${escaped}:\\s*'([^']+)'`, 'm'));
172
228
  if (entryMatch) {
173
229
  const [ts, phase, agent] = entryMatch[1].split('|');
174
- const elapsed = now - parseInt(ts);
230
+ const lastTime = new Date(ts).getTime();
231
+ const elapsed = isNaN(lastTime) ? Infinity : Date.now() - lastTime;
175
232
  const stateChanged = `${phase}|${agent}` !== currentState;
176
233
  shouldPing = elapsed > 300000 || stateChanged; // 5 min or state change
177
234
  } else {
@@ -182,16 +239,24 @@ async function main() {
182
239
  }
183
240
 
184
241
  if (shouldPing) {
185
- // Update ping timestamp
242
+ // Update ping entry — preserve other projects in the file.
186
243
  mkdirSync(join(homedir, '.chati-dev'), { recursive: true });
187
- writeFileSync(pingsPath, `'${projectDir}': '${now}|${currentState}'\n`, 'utf-8');
244
+ const newEntry = `${projectDir}: '${nowIso}|${currentState}'\n`;
245
+ let content = existsSync(pingsPath) ? readFileSync(pingsPath, 'utf-8') : '';
246
+ const escaped = projectDir.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
247
+ if (new RegExp(`^${escaped}:`, 'm').test(content)) {
248
+ content = content.replace(new RegExp(`^${escaped}:.*$`, 'm'), newEntry.trim());
249
+ } else {
250
+ content += newEntry;
251
+ }
252
+ writeFileSync(pingsPath, content, 'utf-8');
188
253
 
189
254
  // Fire-and-forget telemetry (3s timeout, never blocks)
190
255
  try {
191
256
  const body = JSON.stringify({
192
257
  license_key: licenseKey,
193
258
  project_name: digest.currentAgent || 'unknown',
194
- events: [{ type: 'session_active', timestamp: new Date().toISOString(), properties: { pipeline_phase: digest.mode, current_agent: digest.currentAgent } }],
259
+ events: [{ type: 'session_active', timestamp: nowIso, properties: { pipeline_phase: digest.mode, current_agent: digest.currentAgent } }],
195
260
  });
196
261
  fetch('https://chati.dev/api/telemetry', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body, signal: AbortSignal.timeout(3000) }).catch(() => {});
197
262
  } catch { /* expected: network may be unavailable */ }
@@ -202,15 +267,24 @@ async function main() {
202
267
  process.stderr.write(`[chati] session-digest telemetry-ping: ${err.message}\n`);
203
268
  }
204
269
 
205
- process.stdout.write(JSON.stringify({ result: 'allow' }));
270
+ // PreCompact is observational only — empty output is the canonical "no-op".
271
+ process.stdout.write('{}');
206
272
  } catch (err) {
207
273
  process.stderr.write(`[chati] session-digest: ${err.message}\n`);
208
- process.stdout.write(JSON.stringify({ result: 'allow' }));
274
+ process.stdout.write('{}');
209
275
  }
210
276
  }
211
277
 
212
- // Only run main when executed directly (not imported by tests)
278
+ // Only run main when executed directly (not imported by tests).
279
+ // realpathSync resolves symlinks on both sides — important on macOS where
280
+ // /tmp is a symlink to /private/tmp. Without realpath the comparison fails
281
+ // silently and main() never runs.
213
282
  import { fileURLToPath } from 'url';
214
- if (process.argv[1] === fileURLToPath(import.meta.url)) {
215
- main();
283
+ import { realpathSync } from 'fs';
284
+ if (process.argv[1]) {
285
+ try {
286
+ if (realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url))) {
287
+ main();
288
+ }
289
+ } catch { /* path resolution failed — not invoked as a script */ }
216
290
  }