synabun 1.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 (187) hide show
  1. package/CHANGELOG.md +665 -0
  2. package/LICENSE +191 -0
  3. package/README.md +853 -0
  4. package/hooks/claude-code/post-plan.mjs +391 -0
  5. package/hooks/claude-code/post-remember.mjs +239 -0
  6. package/hooks/claude-code/pre-compact.mjs +241 -0
  7. package/hooks/claude-code/pre-websearch.mjs +90 -0
  8. package/hooks/claude-code/prompt-submit.mjs +861 -0
  9. package/hooks/claude-code/session-start.mjs +494 -0
  10. package/hooks/claude-code/shared.mjs +368 -0
  11. package/hooks/claude-code/stop.mjs +646 -0
  12. package/hooks/claude-code/test-user-learning.mjs +531 -0
  13. package/mcp-server/package.json +38 -0
  14. package/mcp-server/run.mjs +10 -0
  15. package/mcp-server/src/config.ts +70 -0
  16. package/mcp-server/src/http.ts +93 -0
  17. package/mcp-server/src/index.ts +203 -0
  18. package/mcp-server/src/preload-http.ts +25 -0
  19. package/mcp-server/src/preload.ts +29 -0
  20. package/mcp-server/src/scripts/migrate-qdrant-to-sqlite.ts +506 -0
  21. package/mcp-server/src/services/categories.ts +429 -0
  22. package/mcp-server/src/services/discord.ts +702 -0
  23. package/mcp-server/src/services/file-checksums.ts +33 -0
  24. package/mcp-server/src/services/local-embeddings.ts +77 -0
  25. package/mcp-server/src/services/neural-interface.ts +344 -0
  26. package/mcp-server/src/services/sqlite.ts +823 -0
  27. package/mcp-server/src/tools/browser-advanced.ts +123 -0
  28. package/mcp-server/src/tools/browser-interact.ts +255 -0
  29. package/mcp-server/src/tools/browser-navigate.ts +77 -0
  30. package/mcp-server/src/tools/browser-observe.ts +1330 -0
  31. package/mcp-server/src/tools/browser.ts +107 -0
  32. package/mcp-server/src/tools/card-tools.ts +193 -0
  33. package/mcp-server/src/tools/card.ts +22 -0
  34. package/mcp-server/src/tools/category.ts +379 -0
  35. package/mcp-server/src/tools/discord-channel.ts +274 -0
  36. package/mcp-server/src/tools/discord-guild.ts +188 -0
  37. package/mcp-server/src/tools/discord-member.ts +158 -0
  38. package/mcp-server/src/tools/discord-message.ts +231 -0
  39. package/mcp-server/src/tools/discord-onboarding.ts +228 -0
  40. package/mcp-server/src/tools/discord-role.ts +209 -0
  41. package/mcp-server/src/tools/discord-thread.ts +110 -0
  42. package/mcp-server/src/tools/discord-webhook.ts +176 -0
  43. package/mcp-server/src/tools/discord.ts +40 -0
  44. package/mcp-server/src/tools/forget.ts +38 -0
  45. package/mcp-server/src/tools/git-tools.ts +141 -0
  46. package/mcp-server/src/tools/git.ts +13 -0
  47. package/mcp-server/src/tools/image-tools.ts +98 -0
  48. package/mcp-server/src/tools/image.ts +17 -0
  49. package/mcp-server/src/tools/leonardo-browser-tools.ts +281 -0
  50. package/mcp-server/src/tools/leonardo.ts +28 -0
  51. package/mcp-server/src/tools/loop.ts +348 -0
  52. package/mcp-server/src/tools/memories.ts +118 -0
  53. package/mcp-server/src/tools/recall.ts +323 -0
  54. package/mcp-server/src/tools/reflect.ts +145 -0
  55. package/mcp-server/src/tools/remember.ts +115 -0
  56. package/mcp-server/src/tools/response.ts +26 -0
  57. package/mcp-server/src/tools/restore.ts +34 -0
  58. package/mcp-server/src/tools/sync.ts +114 -0
  59. package/mcp-server/src/tools/tictactoe-tools.ts +98 -0
  60. package/mcp-server/src/tools/tictactoe.ts +13 -0
  61. package/mcp-server/src/tools/utils.ts +25 -0
  62. package/mcp-server/src/tools/whiteboard-tools.ts +260 -0
  63. package/mcp-server/src/tools/whiteboard.ts +22 -0
  64. package/mcp-server/src/tui.ts +940 -0
  65. package/mcp-server/src/types.ts +85 -0
  66. package/mcp-server/tsconfig.json +19 -0
  67. package/neural-interface/package.json +27 -0
  68. package/neural-interface/public/apple-touch-icon.png +0 -0
  69. package/neural-interface/public/category-logos/claude.svg +1 -0
  70. package/neural-interface/public/category-logos/gemini-color.svg +1 -0
  71. package/neural-interface/public/category-logos/openai.svg +1 -0
  72. package/neural-interface/public/category-logos/synabun.png +0 -0
  73. package/neural-interface/public/claude-chat.css +878 -0
  74. package/neural-interface/public/claude-chat.html +92 -0
  75. package/neural-interface/public/claude-chat.js +1168 -0
  76. package/neural-interface/public/favicon-16x16.png +0 -0
  77. package/neural-interface/public/favicon-32x32.png +0 -0
  78. package/neural-interface/public/gallery.svg +6 -0
  79. package/neural-interface/public/icon-192.png +0 -0
  80. package/neural-interface/public/icon-512.png +0 -0
  81. package/neural-interface/public/index-legacy.html +14677 -0
  82. package/neural-interface/public/index.html +212 -0
  83. package/neural-interface/public/index2d-legacy.html +8193 -0
  84. package/neural-interface/public/index2d.html +127 -0
  85. package/neural-interface/public/invite.html +351 -0
  86. package/neural-interface/public/leonardoai.svg +1 -0
  87. package/neural-interface/public/logo-synabun.png +0 -0
  88. package/neural-interface/public/logoHD.png +0 -0
  89. package/neural-interface/public/manifest.json +14 -0
  90. package/neural-interface/public/offline.html +299 -0
  91. package/neural-interface/public/onboarding.html +2335 -0
  92. package/neural-interface/public/openclaw-logo-text.png +0 -0
  93. package/neural-interface/public/pin-svgrepo-com.svg +25 -0
  94. package/neural-interface/public/sessionicon.svg +8 -0
  95. package/neural-interface/public/shared/ansi-parser.js +305 -0
  96. package/neural-interface/public/shared/api.js +1043 -0
  97. package/neural-interface/public/shared/colors.js +153 -0
  98. package/neural-interface/public/shared/constants.js +141 -0
  99. package/neural-interface/public/shared/html-shell.js +777 -0
  100. package/neural-interface/public/shared/html-term-renderer.js +1022 -0
  101. package/neural-interface/public/shared/i18n.js +147 -0
  102. package/neural-interface/public/shared/registry.js +82 -0
  103. package/neural-interface/public/shared/state.js +86 -0
  104. package/neural-interface/public/shared/storage.js +150 -0
  105. package/neural-interface/public/shared/styles.css +21212 -0
  106. package/neural-interface/public/shared/ui-automation-studio.js +3573 -0
  107. package/neural-interface/public/shared/ui-bookmarks.js +231 -0
  108. package/neural-interface/public/shared/ui-claude-panel.js +6333 -0
  109. package/neural-interface/public/shared/ui-command-runner.js +534 -0
  110. package/neural-interface/public/shared/ui-cost-widget.js +602 -0
  111. package/neural-interface/public/shared/ui-detail.js +1454 -0
  112. package/neural-interface/public/shared/ui-explorer.js +1356 -0
  113. package/neural-interface/public/shared/ui-file-explorer.js +2750 -0
  114. package/neural-interface/public/shared/ui-games.js +411 -0
  115. package/neural-interface/public/shared/ui-help.js +107 -0
  116. package/neural-interface/public/shared/ui-image-gallery.js +461 -0
  117. package/neural-interface/public/shared/ui-invite.js +519 -0
  118. package/neural-interface/public/shared/ui-keybinds.js +452 -0
  119. package/neural-interface/public/shared/ui-layouts.js +297 -0
  120. package/neural-interface/public/shared/ui-link.js +731 -0
  121. package/neural-interface/public/shared/ui-loading.js +255 -0
  122. package/neural-interface/public/shared/ui-menubar.js +499 -0
  123. package/neural-interface/public/shared/ui-multiselect.js +93 -0
  124. package/neural-interface/public/shared/ui-navbar.js +222 -0
  125. package/neural-interface/public/shared/ui-notifications.js +287 -0
  126. package/neural-interface/public/shared/ui-panels.js +309 -0
  127. package/neural-interface/public/shared/ui-resume.js +653 -0
  128. package/neural-interface/public/shared/ui-search.js +145 -0
  129. package/neural-interface/public/shared/ui-sessions.js +531 -0
  130. package/neural-interface/public/shared/ui-settings.js +5987 -0
  131. package/neural-interface/public/shared/ui-sidebar.js +1905 -0
  132. package/neural-interface/public/shared/ui-skills.js +2805 -0
  133. package/neural-interface/public/shared/ui-stats.js +38 -0
  134. package/neural-interface/public/shared/ui-sync.js +201 -0
  135. package/neural-interface/public/shared/ui-terminal.js +6385 -0
  136. package/neural-interface/public/shared/ui-tooltip.js +123 -0
  137. package/neural-interface/public/shared/ui-trash.js +439 -0
  138. package/neural-interface/public/shared/ui-tutorial-draw.js +875 -0
  139. package/neural-interface/public/shared/ui-tutorial-steps.js +2276 -0
  140. package/neural-interface/public/shared/ui-tutorial.js +1117 -0
  141. package/neural-interface/public/shared/ui-update.js +244 -0
  142. package/neural-interface/public/shared/ui-whiteboard.js +3262 -0
  143. package/neural-interface/public/shared/ui-workspaces.js +722 -0
  144. package/neural-interface/public/shared/utils.js +360 -0
  145. package/neural-interface/public/shared/vterm-buffer.js +1102 -0
  146. package/neural-interface/public/sw.js +52 -0
  147. package/neural-interface/public/synabun.png +0 -0
  148. package/neural-interface/public/toolbar-album.svg +8 -0
  149. package/neural-interface/public/toolbar-question.svg +6 -0
  150. package/neural-interface/public/toolbar-sessions.svg +6 -0
  151. package/neural-interface/public/variant/2d/background.js +113 -0
  152. package/neural-interface/public/variant/2d/context-menu.js +133 -0
  153. package/neural-interface/public/variant/2d/gfx.js +45 -0
  154. package/neural-interface/public/variant/2d/graph.js +1635 -0
  155. package/neural-interface/public/variant/2d/hulls.js +207 -0
  156. package/neural-interface/public/variant/2d/lasso.js +135 -0
  157. package/neural-interface/public/variant/2d/layout.js +594 -0
  158. package/neural-interface/public/variant/2d/main.js +521 -0
  159. package/neural-interface/public/variant/2d/minimap.js +312 -0
  160. package/neural-interface/public/variant/2d/settings-gfx.js +98 -0
  161. package/neural-interface/public/variant/3d/background.js +884 -0
  162. package/neural-interface/public/variant/3d/camera.js +661 -0
  163. package/neural-interface/public/variant/3d/gfx.js +116 -0
  164. package/neural-interface/public/variant/3d/graph.js +2399 -0
  165. package/neural-interface/public/variant/3d/main.js +500 -0
  166. package/neural-interface/public/variant/3d/settings-gfx.js +228 -0
  167. package/neural-interface/scripts/rebuild-pty.js +100 -0
  168. package/neural-interface/server.js +15000 -0
  169. package/neural-interface/templates/CLAUDE-template.md +211 -0
  170. package/package.json +53 -0
  171. package/postinstall.js +135 -0
  172. package/setup.js +260 -0
  173. package/skills/leonardo/SKILL.md +159 -0
  174. package/skills/leonardo/icon.svg +1 -0
  175. package/skills/leonardo/modules/browser-guide.md +378 -0
  176. package/skills/leonardo/modules/image-prompter.md +277 -0
  177. package/skills/leonardo/modules/model-advisor.md +100 -0
  178. package/skills/leonardo/modules/prompt-library.md +110 -0
  179. package/skills/leonardo/modules/style-guide.md +186 -0
  180. package/skills/leonardo/modules/video-prompter.md +311 -0
  181. package/skills/synabun/SKILL.md +185 -0
  182. package/skills/synabun/icon.png +0 -0
  183. package/skills/synabun/modules/audit.md +374 -0
  184. package/skills/synabun/modules/changelog.md +194 -0
  185. package/skills/synabun/modules/idea.md +144 -0
  186. package/skills/synabun/modules/memorize.md +188 -0
  187. package/skills/synabun/synabunicon.png +0 -0
@@ -0,0 +1,861 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * SynaBun UserPromptSubmit Hook for Claude Code
5
+ *
6
+ * Fires on every user message. Analyzes the prompt against tiered
7
+ * trigger patterns and injects context-aware recall nudges:
8
+ *
9
+ * TIER 1 (MUST recall) — Past work, decisions, explicit memory references
10
+ * TIER 2 (SHOULD recall) — Debugging, architecture, domain-specific knowledge
11
+ * TIER 3 (CONSIDER recall) — New features, similarity, broad technical mentions
12
+ *
13
+ * Conversation recall triggers have highest priority (above all tiers).
14
+ *
15
+ * Lightweight — reads stdin, analyzes the prompt, outputs additionalContext
16
+ * only when a recall-worthy signal is detected.
17
+ */
18
+
19
+ import { readFileSync, writeFileSync, appendFileSync, existsSync, mkdirSync, renameSync, readdirSync } from 'node:fs';
20
+ import { dirname, join } from 'node:path';
21
+ import { fileURLToPath } from 'node:url';
22
+ import { detectProject } from './shared.mjs';
23
+
24
+ const __dirname = dirname(fileURLToPath(import.meta.url));
25
+ const DATA_DIR = join(__dirname, '..', '..', 'data');
26
+ const HOOK_FEATURES_PATH = join(DATA_DIR, 'hook-features.json');
27
+ const PENDING_REMEMBER_DIR = join(DATA_DIR, 'pending-remember');
28
+ const LOOP_DIR = join(DATA_DIR, 'loop');
29
+
30
+ function getHookFeatures() {
31
+ try {
32
+ if (!existsSync(HOOK_FEATURES_PATH)) return {};
33
+ return JSON.parse(readFileSync(HOOK_FEATURES_PATH, 'utf-8'));
34
+ } catch { return {}; }
35
+ }
36
+
37
+ function readStdin() {
38
+ return new Promise((resolve) => {
39
+ if (process.stdin.isTTY) return resolve('{}');
40
+ let data = '';
41
+ process.stdin.setEncoding('utf8');
42
+ process.stdin.on('data', (chunk) => { data += chunk; });
43
+ process.stdin.on('end', () => resolve(data));
44
+ setTimeout(() => resolve(data || '{}'), 2000);
45
+ });
46
+ }
47
+
48
+ // ── Loop helper functions ──────────────────────────────────────
49
+
50
+ /**
51
+ * Extract formatting/style rules from the task text into a separate block.
52
+ * Matches lines containing prohibitions about dashes, emojis, spam, double posting, etc.
53
+ */
54
+ function extractFormattingRules(task) {
55
+ if (!task) return '';
56
+ const rules = [];
57
+ let hasDashRule = false;
58
+ for (const line of task.split('\n')) {
59
+ const t = line.trim();
60
+ if (!t) continue;
61
+ if (/\b(do not|don'?t|never|must not|avoid)\b/i.test(t) &&
62
+ /\b(dash|--|—|emoji|emote|emojis|spam|double post|over use)\b/i.test(t)) {
63
+ rules.push(t.replace(/^[-*•]\s*/, '').replace(/^\d+\.\s*/, ''));
64
+ if (/dash|--/i.test(t)) hasDashRule = true;
65
+ }
66
+ }
67
+ // Strengthen dash rule with explicit variants
68
+ if (hasDashRule) {
69
+ rules.push('NEVER use double dashes (--), em dashes (\u2014), or en dashes (\u2013) in ANY text you write. Use commas, periods, or semicolons instead.');
70
+ }
71
+ if (rules.length === 0) return '';
72
+ return rules.map(r => `- ${r}`).join('\n');
73
+ }
74
+
75
+ function buildBrowserNote(state) {
76
+ if (!state.usesBrowser) return '';
77
+ const lines = [
78
+ '',
79
+ '=== BROWSER ENFORCEMENT (MANDATORY) ===',
80
+ 'This automation REQUIRES the SynaBun internal browser. You MUST:',
81
+ ];
82
+ // Pin to dedicated browser session if available (multi-automation isolation)
83
+ if (state.browserSessionId) {
84
+ lines.push(`YOUR BROWSER SESSION ID: ${state.browserSessionId}`);
85
+ lines.push('Pass sessionId: "' + state.browserSessionId + '" to ALL browser tool calls (browser_navigate, browser_click, etc.).');
86
+ }
87
+ lines.push(
88
+ '1. Call `browser_navigate` with your target URL to create or reuse a browser session',
89
+ '2. Use ONLY SynaBun MCP browser tools: browser_navigate, browser_go_back, browser_go_forward, browser_reload, browser_click, browser_fill, browser_type, browser_hover, browser_select, browser_press, browser_scroll, browser_upload, browser_snapshot, browser_content, browser_screenshot, browser_evaluate, browser_wait, browser_session, browser_extract_tweets, browser_extract_fb_posts, browser_extract_tiktok_videos, browser_extract_tiktok_search, browser_extract_tiktok_studio, browser_extract_tiktok_profile, browser_extract_wa_chats, browser_extract_wa_messages, browser_extract_ig_feed, browser_extract_ig_profile, browser_extract_ig_post, browser_extract_ig_reels, browser_extract_ig_search, browser_extract_li_feed, browser_extract_li_profile, browser_extract_li_post, browser_extract_li_notifications, browser_extract_li_messages, browser_extract_li_search_people, browser_extract_li_network, browser_extract_li_jobs',
90
+ '3. NEVER use Playwright plugin tools (mcp__plugin_playwright_*) — they launch a separate browser that the user cannot see',
91
+ '4. NEVER use WebFetch or WebSearch tools for tasks that require visual browsing — use the SynaBun browser instead',
92
+ '5. If the browser shows a login page, CAPTCHA, or any wall requiring human action: STOP immediately. Report the blocker to the user and WAIT. Do NOT abandon the browser and fall back to web search. The user can interact with the browser panel to resolve it.',
93
+ 'A persistent Chrome profile is active with saved logins and cookies. The user can see and interact with the SynaBun browser panel.',
94
+ '=== END BROWSER ENFORCEMENT ===',
95
+ );
96
+ return lines.join('\n');
97
+ }
98
+
99
+ function buildAutonomyBlock(blockerRule, sessionId) {
100
+ const updateCall = sessionId
101
+ ? `- After completing this iteration, call \`loop\` with action \`update\`, session_id \`${sessionId}\`, and a brief summary.`
102
+ : '- After completing this iteration, call `loop` with action `update` and a brief summary.';
103
+ return [
104
+ '',
105
+ '--- LOOP AUTONOMY MODE ---',
106
+ 'IMPORTANT: IGNORE any GREETING DIRECTIVE or recall instructions from SessionStart. Do NOT output a greeting. Do NOT call recall. You are in an autonomous loop \u2014 execute the task below immediately.',
107
+ '',
108
+ 'Rules for this session:',
109
+ '- Execute the task directly. Do NOT ask for confirmation or clarification.',
110
+ '- Make reasonable assumptions and proceed. Do not hesitate.',
111
+ '- Use all available tools (browser, memory, file system) as needed without asking.',
112
+ '- Each iteration should produce concrete output or progress.',
113
+ '- If something fails due to a technical issue, try an alternative approach.',
114
+ updateCall,
115
+ '- The server will automatically advance to the next iteration when you finish.',
116
+ blockerRule || '',
117
+ '--- END LOOP AUTONOMY ---',
118
+ ].filter(Boolean).join('\n');
119
+ }
120
+
121
+ function buildJournalBlock(state) {
122
+ const parts = [];
123
+ if (state.progressSummary) {
124
+ parts.push(`PROGRESS SO FAR: ${state.progressSummary}`);
125
+ }
126
+ const journal = Array.isArray(state.journal) ? state.journal : [];
127
+ const recent = journal.slice(-3);
128
+ if (recent.length > 0) {
129
+ parts.push('RECENT ITERATIONS:');
130
+ for (const entry of recent) {
131
+ parts.push(` Iteration ${entry.iteration}: ${entry.summary}`);
132
+ }
133
+ }
134
+ return parts.length > 0 ? parts.join('\n') : '';
135
+ }
136
+
137
+ /**
138
+ * Find an active loop owned by this session (exact file name match only).
139
+ * Does NOT scan other sessions' loop files — cross-session injection is
140
+ * the source of loop leaks between concurrent Claude panels/TUI sessions.
141
+ * Returns the loop state, or null if this session has no active loop.
142
+ */
143
+ function findActiveLoop(sessionId) {
144
+ try {
145
+ const exactPath = join(LOOP_DIR, `${sessionId}.json`);
146
+ if (!existsSync(exactPath)) return null;
147
+ const candidate = JSON.parse(readFileSync(exactPath, 'utf-8'));
148
+ if (!candidate.active) return null;
149
+ // Skip loops inactive for >45 minutes (stuck)
150
+ const lastAct = new Date(candidate.lastIterationAt || candidate.startedAt || 0).getTime();
151
+ if (Date.now() - lastAct > 45 * 60 * 1000) return null;
152
+ return candidate;
153
+ } catch { return null; }
154
+ }
155
+
156
+ // ============================================================
157
+ // TIER 1 — MUST recall (high confidence: past work, decisions, explicit memory)
158
+ // These indicate the user is referencing prior context that memory likely holds.
159
+ // Threshold: >= 1 match fires. Nudge: mandatory.
160
+ // ============================================================
161
+
162
+ const TIER1_TRIGGERS = [
163
+ // Explicit past work references
164
+ /\b(last time|before we|previously|earlier we|remember when|we (did|had|tried|used|decided|chose))\b/i,
165
+ /\b(why (did|do) we|what happened (to|with)|what was the)\b/i,
166
+ /\b(history|context) (of|about|for|on)\b/i,
167
+
168
+ // Decision recall
169
+ /\bshould (we|i) (use|go with|pick|choose|switch|keep|change|stick with)\b/i,
170
+ /\bwhat('s| is| was) the (best|right|correct|agreed|chosen) (approach|way|method|pattern)\b/i,
171
+ /\bwhat did we (decide|agree|settle) on\b/i,
172
+
173
+ // Explicit memory/recall references
174
+ /\b(do you remember|check (your )?memory|what do you know about|recall what)\b/i,
175
+ /\b(we (already|previously) (fixed|solved|handled|addressed|implemented))\b/i,
176
+ ];
177
+
178
+ // ============================================================
179
+ // TIER 2 — SHOULD recall (medium confidence: debugging, architecture, domains)
180
+ // These suggest memory might hold relevant context. Worth checking.
181
+ // Threshold: >= 1 match fires. Nudge: strong suggestion.
182
+ // ============================================================
183
+
184
+ const TIER2_TRIGGERS = [
185
+ // Debugging (likely to have past bug context)
186
+ /\b(bug|error|broken|crash|not working|doesn't work|keeps? (failing|breaking))\b/i,
187
+ /\b(debug|troubleshoot|investigate|diagnose|root cause)\b/i,
188
+
189
+ // Architecture & structural changes
190
+ /\b(refactor|restructur|migrat|upgrad|deprecat)\b/i,
191
+ /\b(architect|redesign|rearchitect)\b/i,
192
+
193
+ // Decision-making questions (broader than Tier 1)
194
+ /\bwhat('s| is) the (best|right|correct|proper) way to\b/i,
195
+ /\bhow (should|do) (we|i) (handle|approach|structure|organize)\b/i,
196
+
197
+ // Specific technical domains that accumulate knowledge
198
+ /\b(supabase|redis|upstash|sqlite)\b/i,
199
+ /\b(auth(entication|orization)?|session handling|jwt|mfa)\b/i,
200
+ /\b(cron job|ranking|price aggregat|deal(s)? (system|pipeline))\b/i,
201
+ ];
202
+
203
+ // ============================================================
204
+ // TIER 3 — CONSIDER recall (lower confidence: new features, broad tech)
205
+ // These MIGHT benefit from memory but often don't. Requires 2+ matches
206
+ // from this tier, OR 1 Tier 3 + 1 Tier 2 to fire. Nudge: soft.
207
+ // ============================================================
208
+
209
+ const TIER3_TRIGGERS = [
210
+ // New features (might conflict with past decisions)
211
+ /\b(implement|integrate) (a |the |new )?\w+/i,
212
+ /\bnew (feature|component|page|endpoint|hook|service)\b/i,
213
+
214
+ // Building on existing patterns
215
+ /\bsimilar to (the|what|how)\b/i,
216
+ /\bsame (as|way|pattern|approach) (as |we )?\b/i,
217
+ /\bconsistent with\b/i,
218
+
219
+ // Broad technical domains
220
+ /\b(database|cache|caching) (schema|strategy|layer|issue)\b/i,
221
+ /\b(api|endpoint) (design|structure|pattern)\b/i,
222
+ /\b(deploy|deployment|ci\/cd|pipeline) (strategy|process|config)\b/i,
223
+ /\b(config|configuration) (for|of|pattern)\b/i,
224
+ ];
225
+
226
+ // ============================================================
227
+ // CONVERSATION RECALL TRIGGERS (highest priority — above all tiers)
228
+ // ============================================================
229
+
230
+ const CONVERSATION_RECALL_TRIGGERS = [
231
+ /\bremember that (conversation|session|chat|discussion|time)\b/i,
232
+ /\bthat (conversation|session|chat) (about|where|when)\b/i,
233
+ /\b(days?|weeks?) ago.*(conversation|session|worked on|discussed|implemented|built)/i,
234
+ /\bcontinue (where we left off|that session|from last time|from yesterday)\b/i,
235
+ /\bwhat did we (talk|discuss|work on|do|build|implement|fix) (last|yesterday|on|the other)/i,
236
+ /\bfind that (session|conversation|chat) (where|about|when|from)\b/i,
237
+ /\bpick up (where|from) (we|last|that)/i,
238
+ /\b(yesterday|last week|other day).*(session|conversation|worked|discussed|implemented)/i,
239
+ ];
240
+
241
+ // ============================================================
242
+ // SKIP PATTERNS — Messages that never need recall
243
+ // ============================================================
244
+
245
+ const SKIP_PATTERNS = [
246
+ // Trivial confirmations
247
+ /^(yes|no|ok|sure|thanks|ty|thank you|perfect|great|good|nice|cool|got it|yep|nope|nah)\b/i,
248
+ // Continuation commands
249
+ /^(do it|go ahead|proceed|continue|keep going|next|done|stop|cancel|abort|nevermind)\b/i,
250
+ // Empty or whitespace
251
+ /^\s*$/,
252
+ // Slash commands (Claude Code handles these)
253
+ /^\/\w+/,
254
+ // Very short messages (< 8 chars, likely just a word)
255
+ /^.{1,7}$/,
256
+ // Direct file operations (no memory needed)
257
+ /^(read|open|show|cat|look at|check) .+\.\w{1,5}$/i,
258
+ // Run commands
259
+ /^(run|execute|start|npm|node|git|pnpm|yarn|bun) /i,
260
+ ];
261
+
262
+ // ============================================================
263
+ // NUDGE TEMPLATES
264
+ // ============================================================
265
+
266
+ const NUDGE = {
267
+ pendingRemember: (editCount, files) => {
268
+ const fileList = files.length > 0
269
+ ? ` Files: ${files.slice(0, 5).join(', ')}${files.length > 5 ? ` (+${files.length - 5} more)` : ''}.`
270
+ : '';
271
+ const urgency = editCount >= 5
272
+ ? `CRITICAL: ${editCount} file edits`
273
+ : editCount >= 3
274
+ ? `IMPORTANT: ${editCount} file edits`
275
+ : `${editCount} file edit${editCount !== 1 ? 's' : ''}`;
276
+ return [
277
+ `SynaBun TASK BOUNDARY: ${urgency} from your previous work have NOT been stored in memory.`,
278
+ `You MUST call \`remember\` for that completed work BEFORE starting this new task.${fileList}`,
279
+ `Summarize what was done, why, and how — then proceed with the user's new request.`,
280
+ ].join(' ');
281
+ },
282
+
283
+ conversation: [
284
+ `The user is asking about a past conversation. Follow the Conversation Recall Workflow:`,
285
+ `1. Calculate exact dates from relative references (e.g., "4 days ago" → compute the date).`,
286
+ `2. Call \`recall\` with category \`conversations\` and include the topic + calculated date in your query.`,
287
+ `3. Present matching conversations via AskUserQuestion with date, project, and summary for each.`,
288
+ `4. After selection, offer recovery options: "Recover entire context and continue", "Continue with compacted summary", or "Other".`,
289
+ ].join(' '),
290
+
291
+ tier1: [
292
+ `You MUST call \`recall\` before responding to this message.`,
293
+ `The user is referencing past work, prior decisions, or historical context that your persistent memory likely holds.`,
294
+ `Search for: the specific topic mentioned, related past decisions, known issues, or prior implementations.`,
295
+ `Do NOT respond without checking memory first.`,
296
+ ].join(' '),
297
+
298
+ tier2: [
299
+ `Before responding, call \`recall\` to check your persistent memory for relevant context.`,
300
+ `This topic likely has prior knowledge stored — past bugs, architecture decisions, or domain-specific patterns.`,
301
+ `Skip recall only if you already have full context from this session.`,
302
+ ].join(' '),
303
+
304
+ tier3: [
305
+ `Consider calling \`recall\` to check if there's relevant prior context about this topic.`,
306
+ `There may be past decisions or patterns worth reviewing before proceeding.`,
307
+ ].join(' '),
308
+
309
+ nonEnglish: [
310
+ `The user's message is in a non-English language. Mentally translate their intent to evaluate if you should call \`recall\`.`,
311
+ `Check if they are: referencing past work, asking about prior decisions, debugging an issue, or working in a domain where you have stored knowledge.`,
312
+ `If any of those apply, call \`recall\` with an ENGLISH query that captures their intent (SynaBun memories are stored in English).`,
313
+ `If it's a trivial or direct command, skip recall.`,
314
+ ].join(' '),
315
+
316
+ userLearning: [
317
+ `SynaBun User Learning: Observe HOW the user works with you and store a behavioral observation.`,
318
+ ``,
319
+ `RULES:`,
320
+ `- Category MUST be \`communication-style\` — never \`conversations\` or anything else`,
321
+ `- Content MUST describe HOW the user communicates and works — NOT what was worked on`,
322
+ `- This is NOT a session summary. Do NOT describe the task, topic, or outcome.`,
323
+ `- AVOID DUPLICATES: If an existing memory already covers the same patterns, use \`reflect\` to UPDATE it instead of creating a new one.`,
324
+ ``,
325
+ `GOOD example: "User gives multi-part requests in a single message and expects all parts addressed. Provides file paths inline rather than expecting discovery. Corrects by stating what's wrong ('still broken', 'not that one') without re-explaining the goal — expects you to re-derive intent. Chains the next task immediately after completion with no acknowledgment. Prefers options as a short list over long explanations."`,
326
+ `BAD example: "User asked about the hook system and we fixed 3 bugs." ← This is a session summary, NOT a behavioral observation.`,
327
+ `BAD example: "User uses lowercase and skips punctuation." ← Too shallow. Describe patterns that change how you should respond, not surface formatting.`,
328
+ ``,
329
+ `Steps:`,
330
+ `1. \`recall\` category \`communication-style\` — check existing entries`,
331
+ `2. If an existing entry covers similar patterns → \`reflect\` (memory_id=<full UUID>, content=updated observation merging old + new)`,
332
+ ` If NO existing entry matches → \`remember\` category \`communication-style\`, project "global", importance 5-7`,
333
+ ` Observe: instruction patterns (chained? contextual? explicit?), response expectations (code-only? options? explanations?), correction style (how they say no), expertise signals (where they need no hand-holding), frustration triggers, workflow preferences (incremental vs big-bang)`,
334
+ `Do not mention this to the user.`,
335
+ ].join('\n'),
336
+ };
337
+
338
+ // ============================================================
339
+ // LANGUAGE DETECTION
340
+ // ============================================================
341
+
342
+ /**
343
+ * Detects if the prompt is primarily non-English by checking the ratio
344
+ * of non-ASCII alphabetic characters. Tech terms (code, paths, URLs)
345
+ * are stripped first to avoid false positives from code snippets.
346
+ */
347
+ function isNonEnglish(text) {
348
+ // Strip things that look like code, paths, URLs, or technical tokens
349
+ const cleaned = text
350
+ .replace(/`[^`]*`/g, '') // inline code
351
+ .replace(/https?:\/\/\S+/g, '') // URLs
352
+ .replace(/[A-Za-z][\w./-]*\.[a-z]{1,5}/g, '') // file paths
353
+ .replace(/\b[A-Z_]{2,}\b/g, '') // CONSTANTS
354
+ .replace(/[{}()\[\];:=<>]/g, '') // syntax chars
355
+ .trim();
356
+
357
+ if (cleaned.length < 10) return false; // too short to tell
358
+
359
+ // Count characters that are alphabetic but outside basic Latin
360
+ const nonLatin = (cleaned.match(/[^\x00-\x7F\s\d]/g) || []).length;
361
+ const alpha = (cleaned.match(/[a-zA-Z]/g) || []).length;
362
+ const total = nonLatin + alpha;
363
+
364
+ if (total === 0) return false;
365
+
366
+ // If > 40% of alphabetic chars are non-Latin, it's likely non-English
367
+ return (nonLatin / total) > 0.4;
368
+ }
369
+
370
+ /**
371
+ * Checks if a Latin-script prompt looks like English by counting
372
+ * common English function words. If 2+ are found, it's English.
373
+ * This prevents the catch-all from firing on English sentences
374
+ * that simply didn't match any tier pattern.
375
+ */
376
+ const ENGLISH_FUNCTION_WORDS = /\b(the|is|are|was|were|have|has|had|will|would|can|could|should|this|that|with|from|for|not|but|and|it|to|in|on|at|of|my|your|our|we|you|they|do|did|does|get|got|set|let|if|or|an?)\b/gi;
377
+
378
+ function looksEnglish(text) {
379
+ const matches = text.match(ENGLISH_FUNCTION_WORDS) || [];
380
+ return matches.length >= 2;
381
+ }
382
+
383
+ // ============================================================
384
+ // MAIN
385
+ // ============================================================
386
+
387
+ // Max user-learning nudges per session (overridable via hook-features.json)
388
+ const USER_LEARNING_MAX_NUDGES_DEFAULT = 3;
389
+
390
+ /**
391
+ * Debug logger for user-learning nudge diagnostics.
392
+ */
393
+ const UL_DEBUG = join(DATA_DIR, 'user-learning-debug.log');
394
+ function debugUL(msg) {
395
+ try { appendFileSync(UL_DEBUG, `[${new Date().toISOString()}] ${msg}\n`); } catch { /* best effort */ }
396
+ }
397
+
398
+ /**
399
+ * Check if user-learning nudge should fire.
400
+ * Fires at threshold multiples (3, 6, 9...) up to max nudges.
401
+ * Returns nudge text or empty string.
402
+ */
403
+ function checkUserLearning(features, sessionId) {
404
+ if (features.userLearning === false) {
405
+ debugUL(`SKIP: userLearning feature disabled`);
406
+ return '';
407
+ }
408
+ if (!sessionId) {
409
+ debugUL(`SKIP: no sessionId`);
410
+ return '';
411
+ }
412
+
413
+ const threshold = features.userLearningThreshold || 8;
414
+ const maxNudges = features.userLearningMaxNudges || USER_LEARNING_MAX_NUDGES_DEFAULT;
415
+ const flagPath = join(PENDING_REMEMBER_DIR, `${sessionId}.json`);
416
+ if (!existsSync(flagPath)) {
417
+ debugUL(`SKIP: flag file not found at ${flagPath}`);
418
+ return '';
419
+ }
420
+
421
+ let flag;
422
+ try {
423
+ flag = JSON.parse(readFileSync(flagPath, 'utf-8'));
424
+ } catch (e) {
425
+ debugUL(`SKIP: failed to parse flag file: ${e.message}`);
426
+ return '';
427
+ }
428
+
429
+ const msgCount = flag.totalSessionMessages || flag.messageCount || 0;
430
+ const nudgeCount = flag.userLearningNudgeCount || 0;
431
+ const observed = flag.userLearningObserved || false;
432
+
433
+ debugUL(`CHECK: session=${sessionId.slice(0, 8)}... msgCount=${msgCount} threshold=${threshold} nudgeCount=${nudgeCount} maxNudges=${maxNudges} observed=${observed}`);
434
+
435
+ // If a style observation was already stored/updated this session, skip further nudges
436
+ if (observed) {
437
+ debugUL(`SKIP: userLearningObserved=true (already stored this session)`);
438
+ return '';
439
+ }
440
+
441
+ if (nudgeCount >= maxNudges) {
442
+ debugUL(`SKIP: max nudges reached (${nudgeCount} >= ${maxNudges})`);
443
+ return '';
444
+ }
445
+ if (msgCount < threshold) {
446
+ debugUL(`SKIP: msgCount ${msgCount} < threshold ${threshold}`);
447
+ return '';
448
+ }
449
+
450
+ const expectedNudges = Math.floor(msgCount / threshold);
451
+ if (expectedNudges <= nudgeCount) {
452
+ debugUL(`SKIP: expectedNudges ${expectedNudges} <= nudgeCount ${nudgeCount}`);
453
+ return '';
454
+ }
455
+
456
+ debugUL(`FIRE: nudge #${nudgeCount + 1} (expectedNudges=${expectedNudges})`);
457
+
458
+ // Persist nudge count + pending flag (best-effort — don't block nudge on write failure)
459
+ flag.userLearningNudgeCount = nudgeCount + 1;
460
+ flag.userLearningPending = true;
461
+ try {
462
+ writeFileSync(flagPath, JSON.stringify(flag));
463
+ debugUL(`PERSIST: nudgeCount saved as ${nudgeCount + 1}`);
464
+ } catch (e) {
465
+ debugUL(`PERSIST FAILED (nudge still fires): ${e.message}`);
466
+ }
467
+
468
+ // First nudge: full instructions. Subsequent: short reminder.
469
+ if (nudgeCount === 0) {
470
+ return NUDGE.userLearning;
471
+ }
472
+ return `SynaBun User Learning reminder: You've had ${msgCount} exchanges. If you've noticed new behavioral patterns (how they give instructions, correct you, make decisions, or signal frustration), call \`recall\` category \`communication-style\` — then \`reflect\` to update an existing entry, or \`remember\` only if genuinely new. Do NOT create duplicates. Do NOT store surface-level formatting observations.`;
473
+ }
474
+
475
+ // ============================================================
476
+ // AUTO-RECALL — Hook-side memory injection
477
+ // Calls NI server to fetch relevant memories for the user's prompt
478
+ // and formats them for injection into additionalContext.
479
+ // ============================================================
480
+
481
+ function formatAge(isoDate) {
482
+ const diffMs = Date.now() - new Date(isoDate).getTime();
483
+ const days = Math.floor(diffMs / (1000 * 60 * 60 * 24));
484
+ if (days === 0) return 'today';
485
+ if (days === 1) return '1 day ago';
486
+ if (days < 30) return `${days} days ago`;
487
+ const months = Math.floor(days / 30);
488
+ return months === 1 ? '1 month ago' : `${months} months ago`;
489
+ }
490
+
491
+ async function autoRecall(prompt, cwd) {
492
+ try {
493
+ const niUrl = process.env.SYNABUN_NI_URL || 'http://localhost:3344';
494
+ const project = detectProject(cwd);
495
+
496
+ const resp = await fetch(`${niUrl}/api/hook-recall`, {
497
+ method: 'POST',
498
+ headers: { 'Content-Type': 'application/json' },
499
+ body: JSON.stringify({
500
+ query: prompt,
501
+ project: project !== 'global' ? project : undefined,
502
+ limit: 3,
503
+ min_score: 0.4,
504
+ }),
505
+ signal: AbortSignal.timeout(3000),
506
+ });
507
+
508
+ if (!resp.ok) return '';
509
+ const data = await resp.json();
510
+ if (!data.results || data.results.length === 0) return '';
511
+
512
+ const lines = data.results.map((r, i) => {
513
+ const score = (r.score * 100).toFixed(0);
514
+ const age = formatAge(r.created_at);
515
+ const tags = r.tags?.length ? `Tags: ${r.tags.join(', ')}` : '';
516
+ const files = r.related_files?.length ? `Files: ${r.related_files.slice(0, 3).join(', ')}` : '';
517
+ const details = [tags, files].filter(Boolean).join(' | ');
518
+ return `${i + 1}. [${r.category} | importance ${r.importance}, ${age}, ${score}% match] ${r.content}${details ? `\n ${details}` : ''}`;
519
+ });
520
+
521
+ return [
522
+ '=== SynaBun: Related Memories ===',
523
+ ...lines,
524
+ 'These memories may be relevant. Use as context — call recall for deeper search if needed.',
525
+ '=== End Memories ===',
526
+ ].join('\n');
527
+ } catch {
528
+ // NI down, timeout, or error — silently skip
529
+ return '';
530
+ }
531
+ }
532
+
533
+ async function main() {
534
+ let prompt = '';
535
+ let sessionId = '';
536
+ let cwd = '';
537
+ try {
538
+ const raw = await readStdin();
539
+ const input = JSON.parse(raw);
540
+ prompt = input.prompt || '';
541
+ sessionId = input.session_id || '';
542
+ cwd = input.cwd || '';
543
+ } catch { /* proceed with empty */ }
544
+
545
+ const trimmed = prompt.trim();
546
+
547
+ // Session heartbeat to Neural Interface session monitor (fire-and-forget)
548
+ if (sessionId) {
549
+ const niUrl = process.env.SYNABUN_NI_URL || 'http://localhost:3344';
550
+ try {
551
+ fetch(`${niUrl}/api/sessions/heartbeat`, {
552
+ method: 'POST',
553
+ headers: { 'Content-Type': 'application/json' },
554
+ body: JSON.stringify({ claudeSessionId: sessionId }),
555
+ signal: AbortSignal.timeout(2000),
556
+ }).catch(() => {});
557
+ } catch { /* ok */ }
558
+ }
559
+
560
+ // --- Loop marker detection (BEFORE greeting — loops must bypass greeting) ---
561
+ const terminalSessionEnv = process.env.SYNABUN_TERMINAL_SESSION || '';
562
+ if (sessionId && /^\[SynaBun Loop\]/i.test(trimmed)) {
563
+ try {
564
+ if (existsSync(LOOP_DIR)) {
565
+ const pending = readdirSync(LOOP_DIR)
566
+ .filter(f => f.startsWith('pending-') && f.endsWith('.json'));
567
+ // Filter by terminalSessionId when available (multi-loop isolation).
568
+ // Without the env var, fall back to first match (legacy behavior).
569
+ let matchedPending = null;
570
+ if (terminalSessionEnv) {
571
+ for (const pf of pending) {
572
+ try {
573
+ const ps = JSON.parse(readFileSync(join(LOOP_DIR, pf), 'utf-8'));
574
+ if (ps.terminalSessionId === terminalSessionEnv) { matchedPending = pf; break; }
575
+ } catch { continue; }
576
+ }
577
+ } else if (pending.length > 0) {
578
+ matchedPending = pending[0];
579
+ }
580
+ if (matchedPending) {
581
+ const pendingPath = join(LOOP_DIR, matchedPending);
582
+ const targetPath = join(LOOP_DIR, `${sessionId}.json`);
583
+ renameSync(pendingPath, targetPath);
584
+
585
+ const state = JSON.parse(readFileSync(targetPath, 'utf-8'));
586
+ delete state.pending;
587
+ // Set currentIteration to 1 immediately — closes the race window where
588
+ // another session's stop hook fallback scan (which only matches
589
+ // currentIteration === 0) could steal this loop file.
590
+ state.currentIteration = 1;
591
+ // Preserve terminalSessionId — loop driver needs it for session isolation
592
+ writeFileSync(targetPath, JSON.stringify(state, null, 2));
593
+
594
+ const browserNote = buildBrowserNote(state);
595
+ const blockerRule = state.usesBrowser
596
+ ? '- CRITICAL: If the browser shows a login page, CAPTCHA, 2FA, or ANY wall requiring human action — STOP IMMEDIATELY. Output what the user needs to do (e.g. "Please log into Twitter in the browser panel"). Do NOT use WebSearch, WebFetch, or any workaround. Do NOT try to bypass it. Just STOP and WAIT.'
597
+ : '';
598
+ const autonomy = buildAutonomyBlock(blockerRule, sessionId);
599
+
600
+ // Extract formatting rules and place them prominently
601
+ const formattingRules = extractFormattingRules(state.task);
602
+ const fmtBlock = formattingRules
603
+ ? `\n=== FORMATTING RULES (MANDATORY \u2014 EVERY ITERATION) ===\n${formattingRules}\n=== END FORMATTING RULES ===\n`
604
+ : '';
605
+
606
+ process.stdout.write(JSON.stringify({
607
+ hookSpecificOutput: {
608
+ hookEventName: 'UserPromptSubmit',
609
+ additionalContext: `${fmtBlock}SynaBun Loop ACTIVE: ${state.totalIterations} iterations (${state.totalIterations - 1} remaining).\nTask: ${state.task}${state.context ? `\nContext: ${state.context}` : ''}${browserNote}\n\nBegin iteration 1 immediately.${autonomy}`,
610
+ },
611
+ }));
612
+ return;
613
+ }
614
+
615
+ // Case 2: Subsequent iteration — find any active loop state file (after /clear)
616
+ // Note: /clear may change the session ID, so we can't match by sessionId alone.
617
+ // Scan for any active loop with currentIteration > 0.
618
+ let existingPath = join(LOOP_DIR, `${sessionId}.json`);
619
+ let state = null;
620
+ if (existsSync(existingPath)) {
621
+ try { state = JSON.parse(readFileSync(existingPath, 'utf-8')); } catch { /* skip */ }
622
+ }
623
+ // Fallback: scan loop files for an active loop (session ID may have changed after /clear).
624
+ // When SYNABUN_TERMINAL_SESSION is set, only match loops owned by this terminal.
625
+ // Without the env var, fall back to first active loop (legacy behavior).
626
+ if (!state?.active || !(state?.currentIteration > 0)) {
627
+ const now = Date.now();
628
+ const allLoopFiles = readdirSync(LOOP_DIR)
629
+ .filter(f => f.endsWith('.json') && !f.startsWith('pending-'));
630
+ for (const f of allLoopFiles) {
631
+ try {
632
+ const fullPath = join(LOOP_DIR, f);
633
+ const candidate = JSON.parse(readFileSync(fullPath, 'utf-8'));
634
+ if (candidate.active && candidate.currentIteration > 0) {
635
+ // Multi-loop isolation: only match OUR terminal's loop
636
+ if (terminalSessionEnv && candidate.terminalSessionId && candidate.terminalSessionId !== terminalSessionEnv) continue;
637
+ // Validate the loop hasn't exceeded its own time cap + grace
638
+ // Skip loops inactive for >45 minutes (stuck)
639
+ const lastAct = new Date(candidate.lastIterationAt || candidate.startedAt || 0).getTime();
640
+ if (now - lastAct > 45 * 60 * 1000) continue;
641
+ state = candidate;
642
+ existingPath = fullPath;
643
+ break;
644
+ }
645
+ } catch { /* skip corrupt */ }
646
+ }
647
+ }
648
+ if (state?.active && state?.currentIteration > 0) {
649
+ const formattingRules = extractFormattingRules(state.task);
650
+ const browserNote = buildBrowserNote(state);
651
+ const blockerRule2 = state.usesBrowser
652
+ ? '- CRITICAL: If the browser shows a login page, CAPTCHA, 2FA, or ANY wall requiring human action — STOP IMMEDIATELY. Output what the user needs to do. Do NOT use WebSearch, WebFetch, or any workaround. Just STOP and WAIT.'
653
+ : '';
654
+ const autonomy2 = buildAutonomyBlock(blockerRule2, sessionId);
655
+ const journal = buildJournalBlock(state);
656
+ const iterationsRemaining = (state.totalIterations || 10) - (state.currentIteration || 0);
657
+
658
+ const parts = [
659
+ // Memory rules (session-start won't re-inject after /clear)
660
+ 'SynaBun memory is active. CLAUDE.md contains the memory rules. Follow them.',
661
+ '',
662
+ ];
663
+
664
+ // Formatting rules — FIRST, most prominent position
665
+ if (formattingRules) {
666
+ parts.push(
667
+ '=== FORMATTING RULES (MANDATORY \u2014 EVERY ITERATION) ===',
668
+ formattingRules,
669
+ '=== END FORMATTING RULES ===',
670
+ '',
671
+ );
672
+ }
673
+
674
+ parts.push(
675
+ `SynaBun Loop ACTIVE: Iteration ${state.currentIteration}/${state.totalIterations} (${iterationsRemaining} remaining).`,
676
+ `Task: ${state.task}`,
677
+ );
678
+ if (state.context) parts.push(`Context: ${state.context}`);
679
+
680
+ // Journal + progress
681
+ if (journal) parts.push('', journal);
682
+
683
+ // Browser enforcement
684
+ if (browserNote) parts.push(browserNote);
685
+
686
+ parts.push(
687
+ '',
688
+ `Begin iteration ${state.currentIteration} immediately.`,
689
+ autonomy2,
690
+ );
691
+
692
+ process.stdout.write(JSON.stringify({
693
+ hookSpecificOutput: {
694
+ hookEventName: 'UserPromptSubmit',
695
+ additionalContext: parts.filter(Boolean).join('\n'),
696
+ },
697
+ }));
698
+ return;
699
+ }
700
+ }
701
+ } catch { /* fall through to normal processing */ }
702
+ }
703
+
704
+ // --- Active loop detection for non-loop-marker messages ---
705
+ // When user sends a regular message during an active browser loop, inject
706
+ // loop context so Claude knows where it was, and sync the session ID file.
707
+ let activeLoopNotice = '';
708
+ if (sessionId && !/^\[SynaBun Loop\]/i.test(trimmed)) {
709
+ const activeLoop = findActiveLoop(sessionId);
710
+ if (activeLoop) {
711
+ const journal = buildJournalBlock(activeLoop);
712
+ const iterLeft = (activeLoop.totalIterations || 10) - (activeLoop.currentIteration || 0);
713
+ const browserNote = activeLoop.usesBrowser ? buildBrowserNote(activeLoop) : '';
714
+ const parts = [
715
+ `=== ACTIVE LOOP NOTICE ===`,
716
+ `You are mid-loop: Iteration ${activeLoop.currentIteration}/${activeLoop.totalIterations} (${iterLeft} iterations remaining).`,
717
+ `Task: ${activeLoop.task}`,
718
+ ];
719
+ if (activeLoop.context) parts.push(`Context: ${activeLoop.context}`);
720
+ if (journal) parts.push('', journal);
721
+ parts.push(
722
+ '',
723
+ `The user sent a message. Respond to it, then call \`loop\` action \`update\` with your current progress, then continue the loop task from where you left off.`,
724
+ );
725
+ if (browserNote) parts.push(browserNote);
726
+ parts.push(`=== END LOOP NOTICE ===`);
727
+ activeLoopNotice = parts.filter(p => p !== undefined).join('\n');
728
+ }
729
+ }
730
+
731
+ // --- Track message count (BEFORE skip check — all messages count) ---
732
+ let currentMessageCount = 0;
733
+ if (sessionId && trimmed.length > 0) {
734
+ const flagPath = join(PENDING_REMEMBER_DIR, `${sessionId}.json`);
735
+ if (!existsSync(PENDING_REMEMBER_DIR)) mkdirSync(PENDING_REMEMBER_DIR, { recursive: true });
736
+ let flag = { editCount: 0, retries: 0, files: [], messageCount: 0 };
737
+ if (existsSync(flagPath)) {
738
+ try { flag = JSON.parse(readFileSync(flagPath, 'utf-8')); } catch { /* start fresh */ }
739
+ }
740
+ flag.messageCount = (flag.messageCount || 0) + 1;
741
+ flag.totalSessionMessages = (flag.totalSessionMessages || 0) + 1;
742
+ currentMessageCount = flag.messageCount;
743
+ if (!flag.firstMessageAt) flag.firstMessageAt = new Date().toISOString();
744
+ flag.lastMessageAt = new Date().toISOString();
745
+ try { writeFileSync(flagPath, JSON.stringify(flag)); } catch { /* ok */ }
746
+
747
+ // --- Greeting reinforcement (first message only, before skip patterns) ---
748
+ if (flag.messageCount === 1 && !flag.greetingDelivered) {
749
+ const greetingFeatures = getHookFeatures();
750
+ if (greetingFeatures.greeting === true) {
751
+ flag.greetingDelivered = true;
752
+ try { writeFileSync(flagPath, JSON.stringify(flag)); } catch { /* ok */ }
753
+ process.stdout.write(JSON.stringify({
754
+ hookSpecificOutput: {
755
+ hookEventName: 'UserPromptSubmit',
756
+ additionalContext: 'SynaBun: This is the FIRST message of the session. Output the greeting from the GREETING DIRECTIVE in your session context NOW, then respond to the user\'s message. Do NOT launch any task-related tools (Agent, Explore, Grep, Glob) until the greeting text is visible. If their message is just a greeting (hi, hello, hey, or a single character), the greeting IS your full response — no need to ask what they need.',
757
+ },
758
+ }));
759
+ return;
760
+ }
761
+ }
762
+
763
+ }
764
+
765
+ // Skip trivial messages first (fastest path)
766
+ if (SKIP_PATTERNS.some(p => p.test(trimmed))) {
767
+ process.stdout.write(JSON.stringify({}));
768
+ return;
769
+ }
770
+
771
+ const features = getHookFeatures();
772
+
773
+ // --- Collect primary context from priority chain ---
774
+ let primaryContext = '';
775
+
776
+ // Priority 0: TASK BOUNDARY — pending-remember check (highest priority)
777
+ if (!primaryContext && sessionId) {
778
+ const flagPath = join(PENDING_REMEMBER_DIR, `${sessionId}.json`);
779
+ if (existsSync(flagPath)) {
780
+ try {
781
+ const flag = JSON.parse(readFileSync(flagPath, 'utf-8'));
782
+ const editCount = flag.editCount || 0;
783
+ if (editCount >= 1) {
784
+ const files = Array.isArray(flag.files) ? flag.files : [];
785
+ primaryContext = NUDGE.pendingRemember(editCount, files);
786
+ }
787
+ } catch { /* corrupt flag — ignore, don't block */ }
788
+ }
789
+ }
790
+
791
+ // Priority 1: Conversation recall (highest recall priority)
792
+ if (!primaryContext) {
793
+ const conversationMemoryEnabled = features.conversationMemory !== false;
794
+ if (conversationMemoryEnabled) {
795
+ const convMatches = CONVERSATION_RECALL_TRIGGERS.filter(p => p.test(prompt));
796
+ if (convMatches.length >= 1) {
797
+ primaryContext = NUDGE.conversation;
798
+ }
799
+ }
800
+ }
801
+
802
+ // Priority 2: Tier 1 — MUST recall (>= 1 match)
803
+ if (!primaryContext) {
804
+ const t1 = TIER1_TRIGGERS.filter(p => p.test(prompt));
805
+ if (t1.length >= 1) primaryContext = NUDGE.tier1;
806
+ }
807
+
808
+ // Priority 3: Tier 2 — SHOULD recall (>= 1 match)
809
+ if (!primaryContext) {
810
+ const t2 = TIER2_TRIGGERS.filter(p => p.test(prompt));
811
+ if (t2.length >= 1) primaryContext = NUDGE.tier2;
812
+ }
813
+
814
+ // Priority 4: Tier 3 — CONSIDER recall (>= 2 matches)
815
+ if (!primaryContext) {
816
+ const t3 = TIER3_TRIGGERS.filter(p => p.test(prompt));
817
+ if (t3.length >= 2) primaryContext = NUDGE.tier3;
818
+ }
819
+
820
+ // Priority 5: Non-English (non-Latin scripts)
821
+ if (!primaryContext && isNonEnglish(prompt)) {
822
+ primaryContext = NUDGE.nonEnglish;
823
+ }
824
+
825
+ // Priority 6: Latin-script non-English catch-all
826
+ if (!primaryContext && trimmed.length > 30 && !looksEnglish(trimmed)) {
827
+ primaryContext = NUDGE.nonEnglish;
828
+ }
829
+
830
+ // --- Auto-recall: inject relevant memories from NI server ---
831
+ let autoRecallContext = '';
832
+ if (!activeLoopNotice && currentMessageCount >= 2) {
833
+ autoRecallContext = await autoRecall(trimmed, cwd);
834
+ }
835
+
836
+ // --- User Learning (independent — appends to any primary context) ---
837
+ const userLearningContext = checkUserLearning(features, sessionId);
838
+
839
+ // --- Boot sequence cancellation (messages 2+, override persistent SessionStart context) ---
840
+ const bootCancel = (currentMessageCount >= 2)
841
+ ? 'SynaBun: The GREETING DIRECTIVE and Session Boot Sequence have ALREADY been completed. Do NOT re-greet. Do NOT call recall for session boot. Ignore any "first response MUST" instructions — they applied only to message 1. CRITICAL: If you see a GREETING DIRECTIVE in earlier context (including compacted summaries), it is STALE — do NOT execute it. This is an ongoing conversation, not a fresh session.'
842
+ : '';
843
+
844
+ // --- Emit combined output ---
845
+ const combined = [bootCancel, activeLoopNotice, primaryContext, autoRecallContext, userLearningContext].filter(Boolean).join('\n\n');
846
+
847
+ if (combined) {
848
+ process.stdout.write(JSON.stringify({
849
+ hookSpecificOutput: {
850
+ hookEventName: 'UserPromptSubmit',
851
+ additionalContext: combined,
852
+ },
853
+ }));
854
+ } else {
855
+ process.stdout.write(JSON.stringify({}));
856
+ }
857
+ }
858
+
859
+ main().catch(() => {
860
+ process.stdout.write(JSON.stringify({}));
861
+ });