@hailer/mcp 1.0.29 → 1.1.2

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 (233) hide show
  1. package/.claude/.session-checked +1 -0
  2. package/.claude/agents/agent-ada-skill-builder.md +10 -2
  3. package/.claude/agents/agent-alejandro-function-fields.md +104 -37
  4. package/.claude/agents/agent-bjorn-config-audit.md +41 -21
  5. package/.claude/agents/agent-builder-agent-creator.md +13 -3
  6. package/.claude/agents/agent-code-simplifier.md +53 -0
  7. package/.claude/agents/agent-dmitri-activity-crud.md +126 -11
  8. package/.claude/agents/agent-giuseppe-app-builder.md +212 -22
  9. package/.claude/agents/agent-gunther-mcp-tools.md +7 -36
  10. package/.claude/agents/agent-helga-workflow-config.md +75 -10
  11. package/.claude/agents/agent-igor-activity-mover-automation.md +125 -0
  12. package/.claude/agents/agent-ingrid-doc-templates.md +164 -36
  13. package/.claude/agents/agent-ivan-monolith.md +154 -0
  14. package/.claude/agents/agent-kenji-data-reader.md +15 -8
  15. package/.claude/agents/agent-lars-code-inspector.md +56 -8
  16. package/.claude/agents/agent-marco-mockup-builder.md +110 -0
  17. package/.claude/agents/agent-marcus-api-documenter.md +323 -0
  18. package/.claude/agents/agent-marketplace-publisher.md +232 -72
  19. package/.claude/agents/agent-marketplace-reviewer.md +255 -79
  20. package/.claude/agents/agent-permissions-handler.md +208 -0
  21. package/.claude/agents/agent-simple-writer.md +48 -0
  22. package/.claude/agents/agent-svetlana-code-review.md +127 -14
  23. package/.claude/agents/agent-tanya-test-runner.md +333 -0
  24. package/.claude/agents/agent-ui-designer.md +100 -0
  25. package/.claude/agents/agent-viktor-sql-insights.md +19 -6
  26. package/.claude/agents/agent-web-search.md +55 -0
  27. package/.claude/agents/agent-yevgeni-discussions.md +7 -1
  28. package/.claude/agents/agent-zara-zapier.md +159 -0
  29. package/.claude/commands/app-squad.md +135 -0
  30. package/.claude/commands/audit-squad.md +158 -0
  31. package/.claude/commands/autoplan.md +563 -0
  32. package/.claude/commands/cleanup-squad.md +98 -0
  33. package/.claude/commands/config-squad.md +106 -0
  34. package/.claude/commands/crud-squad.md +87 -0
  35. package/.claude/commands/data-squad.md +97 -0
  36. package/.claude/commands/debug-squad.md +303 -0
  37. package/.claude/commands/doc-squad.md +65 -0
  38. package/.claude/commands/handoff.md +137 -0
  39. package/.claude/commands/health.md +49 -0
  40. package/.claude/commands/help.md +2 -1
  41. package/.claude/commands/help:agents.md +96 -16
  42. package/.claude/commands/help:commands.md +55 -11
  43. package/.claude/commands/help:faq.md +16 -1
  44. package/.claude/commands/help:skills.md +93 -0
  45. package/.claude/commands/hotfix-squad.md +112 -0
  46. package/.claude/commands/integration-squad.md +82 -0
  47. package/.claude/commands/janitor-squad.md +167 -0
  48. package/.claude/commands/learn-auto.md +120 -0
  49. package/.claude/commands/learn.md +120 -0
  50. package/.claude/commands/mcp-list.md +27 -0
  51. package/.claude/commands/onboard-squad.md +140 -0
  52. package/.claude/commands/plan-workspace.md +732 -0
  53. package/.claude/commands/prd.md +131 -0
  54. package/.claude/commands/project-status.md +82 -0
  55. package/.claude/commands/publish.md +138 -0
  56. package/.claude/commands/recap.md +69 -0
  57. package/.claude/commands/restore.md +64 -0
  58. package/.claude/commands/review-squad.md +152 -0
  59. package/.claude/commands/save.md +24 -0
  60. package/.claude/commands/stats.md +19 -0
  61. package/.claude/commands/swarm.md +210 -0
  62. package/.claude/commands/tool-builder.md +3 -1
  63. package/.claude/commands/ws-pull.md +1 -1
  64. package/.claude/commands/yolo-off.md +17 -0
  65. package/.claude/commands/yolo.md +82 -0
  66. package/.claude/hooks/_shared-memory.cjs +305 -0
  67. package/.claude/hooks/_utils.cjs +134 -0
  68. package/.claude/hooks/agent-failure-detector.cjs +164 -79
  69. package/.claude/hooks/agent-usage-logger.cjs +204 -0
  70. package/.claude/hooks/app-edit-guard.cjs +20 -4
  71. package/.claude/hooks/auto-learn.cjs +316 -0
  72. package/.claude/hooks/bash-guard.cjs +282 -0
  73. package/.claude/hooks/builder-mode-manager.cjs +183 -54
  74. package/.claude/hooks/bulk-activity-guard.cjs +283 -0
  75. package/.claude/hooks/context-watchdog.cjs +292 -0
  76. package/.claude/hooks/delegation-reminder.cjs +478 -0
  77. package/.claude/hooks/design-system-lint.cjs +283 -0
  78. package/.claude/hooks/post-scaffold-hook.cjs +16 -3
  79. package/.claude/hooks/prompt-guard.cjs +366 -0
  80. package/.claude/hooks/publish-template-guard.cjs +16 -0
  81. package/.claude/hooks/session-start.cjs +35 -0
  82. package/.claude/hooks/shared-memory-writer.cjs +147 -0
  83. package/.claude/hooks/skill-injector.cjs +140 -0
  84. package/.claude/hooks/skill-usage-logger.cjs +258 -0
  85. package/.claude/hooks/src-edit-guard.cjs +16 -1
  86. package/.claude/hooks/sync-marketplace-agents.cjs +53 -8
  87. package/.claude/scripts/yolo-toggle.cjs +142 -0
  88. package/.claude/settings.json +141 -14
  89. package/.claude/skills/SDK-activity-patterns/SKILL.md +428 -0
  90. package/.claude/skills/SDK-document-templates/SKILL.md +1033 -0
  91. package/.claude/skills/SDK-function-fields/SKILL.md +542 -0
  92. package/.claude/skills/SDK-generate-skill/SKILL.md +92 -0
  93. package/.claude/skills/SDK-init-skill/SKILL.md +127 -0
  94. package/.claude/skills/SDK-insight-queries/SKILL.md +787 -0
  95. package/.claude/skills/SDK-ws-config-skill/SKILL.md +1139 -0
  96. package/.claude/skills/agent-structure/SKILL.md +98 -0
  97. package/.claude/skills/api-documentation-patterns/SKILL.md +474 -0
  98. package/.claude/skills/chrome-mcp-reference/SKILL.md +370 -0
  99. package/.claude/skills/delegation-routing/SKILL.md +202 -0
  100. package/.claude/skills/frontend-design/SKILL.md +254 -0
  101. package/.claude/skills/hailer-activity-mover/SKILL.md +213 -0
  102. package/.claude/skills/hailer-api-client/SKILL.md +518 -0
  103. package/.claude/skills/hailer-app-builder/SKILL.md +939 -11
  104. package/.claude/skills/hailer-apps-pictures/SKILL.md +269 -0
  105. package/.claude/skills/hailer-design-system/SKILL.md +235 -0
  106. package/.claude/skills/hailer-monolith-automations/SKILL.md +686 -0
  107. package/.claude/skills/hailer-permissions-system/SKILL.md +121 -0
  108. package/.claude/skills/hailer-project-protocol/SKILL.md +488 -0
  109. package/.claude/skills/hailer-rest-api/SKILL.md +61 -0
  110. package/.claude/skills/hailer-rest-api/hailer-activities.md +184 -0
  111. package/.claude/skills/hailer-rest-api/hailer-admin.md +473 -0
  112. package/.claude/skills/hailer-rest-api/hailer-calendar.md +256 -0
  113. package/.claude/skills/hailer-rest-api/hailer-feed.md +249 -0
  114. package/.claude/skills/hailer-rest-api/hailer-insights.md +195 -0
  115. package/.claude/skills/hailer-rest-api/hailer-messaging.md +276 -0
  116. package/.claude/skills/hailer-rest-api/hailer-workflows.md +283 -0
  117. package/.claude/skills/insight-join-patterns/SKILL.md +3 -0
  118. package/.claude/skills/integration-patterns/SKILL.md +421 -0
  119. package/.claude/skills/json-only-output/SKILL.md +52 -12
  120. package/.claude/skills/lsp-setup/SKILL.md +160 -0
  121. package/.claude/skills/mcp-direct-tools/SKILL.md +153 -0
  122. package/.claude/skills/optional-parameters/SKILL.md +32 -23
  123. package/.claude/skills/publish-hailer-app/SKILL.md +76 -12
  124. package/.claude/skills/testing-patterns/SKILL.md +630 -0
  125. package/.claude/skills/tool-builder/SKILL.md +250 -0
  126. package/.claude/skills/tool-parameter-usage/SKILL.md +59 -45
  127. package/.claude/skills/tool-response-verification/SKILL.md +82 -48
  128. package/.claude/skills/zapier-hailer-patterns/SKILL.md +581 -0
  129. package/.env.example +26 -7
  130. package/CLAUDE.md +290 -224
  131. package/dist/CLAUDE.md +370 -0
  132. package/dist/app.d.ts +1 -1
  133. package/dist/app.js +101 -101
  134. package/dist/bot/bot-config.d.ts +26 -0
  135. package/dist/bot/bot-config.js +135 -0
  136. package/dist/bot/bot-manager.d.ts +40 -0
  137. package/dist/bot/bot-manager.js +137 -0
  138. package/dist/bot/bot.d.ts +127 -0
  139. package/dist/bot/bot.js +1328 -0
  140. package/dist/bot/operation-logger.d.ts +28 -0
  141. package/dist/bot/operation-logger.js +132 -0
  142. package/dist/bot/services/conversation-manager.d.ts +60 -0
  143. package/dist/bot/services/conversation-manager.js +246 -0
  144. package/dist/bot/services/index.d.ts +9 -0
  145. package/dist/bot/services/index.js +18 -0
  146. package/dist/bot/services/message-classifier.d.ts +42 -0
  147. package/dist/bot/services/message-classifier.js +228 -0
  148. package/dist/bot/services/message-formatter.d.ts +88 -0
  149. package/dist/bot/services/message-formatter.js +411 -0
  150. package/dist/bot/services/session-logger.d.ts +162 -0
  151. package/dist/bot/services/session-logger.js +724 -0
  152. package/dist/bot/services/token-billing.d.ts +78 -0
  153. package/dist/bot/services/token-billing.js +233 -0
  154. package/dist/bot/services/types.d.ts +169 -0
  155. package/dist/bot/services/types.js +12 -0
  156. package/dist/bot/services/typing-indicator.d.ts +23 -0
  157. package/dist/bot/services/typing-indicator.js +60 -0
  158. package/dist/bot/services/workspace-schema-cache.d.ts +122 -0
  159. package/dist/bot/services/workspace-schema-cache.js +506 -0
  160. package/dist/bot/tool-executor.d.ts +28 -0
  161. package/dist/bot/tool-executor.js +48 -0
  162. package/dist/bot/workspace-overview.d.ts +12 -0
  163. package/dist/bot/workspace-overview.js +94 -0
  164. package/dist/cli.d.ts +1 -8
  165. package/dist/cli.js +1 -253
  166. package/dist/config.d.ts +96 -3
  167. package/dist/config.js +148 -37
  168. package/dist/core.d.ts +5 -0
  169. package/dist/core.js +61 -8
  170. package/dist/lib/discussion-lock.d.ts +42 -0
  171. package/dist/lib/discussion-lock.js +110 -0
  172. package/dist/lib/logger.d.ts +0 -1
  173. package/dist/lib/logger.js +39 -23
  174. package/dist/lib/request-logger.d.ts +77 -0
  175. package/dist/lib/request-logger.js +147 -0
  176. package/dist/mcp/UserContextCache.js +16 -13
  177. package/dist/mcp/hailer-clients.js +18 -17
  178. package/dist/mcp/signal-handler.js +29 -13
  179. package/dist/mcp/tool-registry.d.ts +4 -15
  180. package/dist/mcp/tool-registry.js +94 -32
  181. package/dist/mcp/tools/activity.js +28 -69
  182. package/dist/mcp/tools/app-core.js +9 -4
  183. package/dist/mcp/tools/app-marketplace.js +22 -12
  184. package/dist/mcp/tools/app-member.js +5 -2
  185. package/dist/mcp/tools/app-scaffold.js +32 -18
  186. package/dist/mcp/tools/bot-config/constants.d.ts +23 -0
  187. package/dist/mcp/tools/bot-config/constants.js +94 -0
  188. package/dist/mcp/tools/bot-config/core.d.ts +253 -0
  189. package/dist/mcp/tools/bot-config/core.js +2456 -0
  190. package/dist/mcp/tools/bot-config/index.d.ts +10 -0
  191. package/dist/mcp/tools/bot-config/index.js +59 -0
  192. package/dist/mcp/tools/bot-config/tools.d.ts +7 -0
  193. package/dist/mcp/tools/bot-config/tools.js +15 -0
  194. package/dist/mcp/tools/bot-config/types.d.ts +50 -0
  195. package/dist/mcp/tools/bot-config/types.js +6 -0
  196. package/dist/mcp/tools/discussion.js +107 -77
  197. package/dist/mcp/tools/document.d.ts +11 -0
  198. package/dist/mcp/tools/document.js +741 -0
  199. package/dist/mcp/tools/file.js +5 -2
  200. package/dist/mcp/tools/insight.js +36 -12
  201. package/dist/mcp/tools/investigate.d.ts +9 -0
  202. package/dist/mcp/tools/investigate.js +254 -0
  203. package/dist/mcp/tools/user.d.ts +2 -4
  204. package/dist/mcp/tools/user.js +9 -50
  205. package/dist/mcp/tools/workflow.d.ts +1 -0
  206. package/dist/mcp/tools/workflow.js +164 -52
  207. package/dist/mcp/utils/hailer-api-client.js +26 -17
  208. package/dist/mcp/webhook-handler.d.ts +64 -3
  209. package/dist/mcp/webhook-handler.js +219 -9
  210. package/dist/mcp-server.d.ts +4 -0
  211. package/dist/mcp-server.js +237 -25
  212. package/dist/plugins/bug-fixer/index.d.ts +2 -0
  213. package/dist/plugins/bug-fixer/index.js +18 -0
  214. package/dist/plugins/bug-fixer/tools.d.ts +45 -0
  215. package/dist/plugins/bug-fixer/tools.js +1096 -0
  216. package/package.json +10 -10
  217. package/scripts/test-hal-tools.ts +154 -0
  218. package/.claude/agents/agent-nora-name-functions.md +0 -123
  219. package/.claude/assistant-knowledge.md +0 -23
  220. package/.claude/commands/install-plugin.md +0 -261
  221. package/.claude/commands/list-plugins.md +0 -42
  222. package/.claude/commands/marketplace-setup.md +0 -33
  223. package/.claude/commands/publish-plugin.md +0 -55
  224. package/.claude/commands/uninstall-plugin.md +0 -87
  225. package/.claude/hooks/interactive-mode.cjs +0 -87
  226. package/.claude/hooks/mcp-server-guard.cjs +0 -108
  227. package/.claude/skills/marketplace-publishing.md +0 -155
  228. package/dist/bot/chat-bot.d.ts +0 -31
  229. package/dist/bot/chat-bot.js +0 -357
  230. package/dist/mcp/tools/metrics.d.ts +0 -13
  231. package/dist/mcp/tools/metrics.js +0 -546
  232. package/dist/stdio-server.d.ts +0 -14
  233. package/dist/stdio-server.js +0 -114
@@ -40,21 +40,47 @@
40
40
 
41
41
  const fs = require('fs');
42
42
  const path = require('path');
43
+ const os = require('os');
44
+
45
+ // Skip in yolo mode
46
+ try {
47
+ const statePath = path.join(process.env.CLAUDE_PROJECT_DIR || process.cwd(), '.claude', 'yolo-state.json');
48
+ const state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
49
+ if (state.mode === 'yolo') process.exit(0);
50
+ } catch (e) {
51
+ // ENOENT is expected when not in yolo mode - only warn on unexpected errors
52
+ if (e.code !== 'ENOENT' && !e.message.includes('Unexpected')) {
53
+ console.error(`[builder-mode-manager] Warning: ${e.message}`);
54
+ }
55
+ }
43
56
 
44
- const BUILDER_MODE_FILE = '/tmp/.claude-builder-agent-active';
45
- const AGENT_STACK_FILE = '/tmp/.claude-agent-stack.json';
57
+ const TEMP_DIR = os.tmpdir();
58
+ const BUILDER_MODE_FILE = path.join(TEMP_DIR, '.claude-builder-agent-active');
59
+ const AGENT_STACK_FILE = path.join(TEMP_DIR, '.claude-agent-stack.json');
60
+ const LOCK_FILE = AGENT_STACK_FILE + '.lock';
61
+ const LOCK_TIMEOUT = 5000; // 5 seconds max wait
46
62
 
47
63
  // Agents that need builder mode (can edit code)
64
+ // Uses partial matching - any subagent_type containing these strings triggers builder mode
48
65
  const CODE_EDITING_AGENTS = [
49
- 'giuseppe', // Hailer app builder
50
- 'gunther', // MCP tool builder
51
- 'general-purpose', // Can do anything
52
- 'agent-builder', // Creates new agents
53
- 'helga', // Workspace config (edits workspace/ files)
54
- 'ingrid', // Document templates
55
- 'ada', // Skills and agent improvements
66
+ 'giuseppe', // agent-giuseppe-app-builder
67
+ 'gunther', // agent-gunther-mcp-tools
68
+ 'general-purpose', // general-purpose agent
69
+ 'agent-builder', // agent-builder-agent-creator
70
+ 'helga', // agent-helga-workflow-config
71
+ 'ingrid', // agent-ingrid-doc-templates
72
+ 'ada', // agent-ada-skill-builder
73
+ 'simple-writer', // agent-simple-writer
74
+ 'code-simplifier', // agent-code-simplifier
75
+ 'marco', // agent-marco-mockup-builder
56
76
  ];
57
77
 
78
+ // NOTE: No in-memory cache - multiple hooks may run concurrently
79
+ // Each hook reads fresh from file to avoid stale reads
80
+
81
+ // Staleness threshold - remove agents older than this (4 hours - long-running agents need time)
82
+ const AGENT_STALENESS_MS = 4 * 60 * 60 * 1000;
83
+
58
84
  // Read hook input from stdin
59
85
  let input = '';
60
86
  process.stdin.setEncoding('utf8');
@@ -65,38 +91,128 @@ process.stdin.on('end', () => {
65
91
  processHook(data);
66
92
  } catch (e) {
67
93
  // Invalid JSON - allow to avoid blocking
94
+ console.error(`[builder-mode-manager] Warning: ${e.message}`);
68
95
  console.log(JSON.stringify({ decision: 'allow' }));
69
96
  process.exit(0);
70
97
  }
71
98
  });
72
99
 
100
+ function acquireLock() {
101
+ const start = Date.now();
102
+ while (Date.now() - start < LOCK_TIMEOUT) {
103
+ try {
104
+ fs.mkdirSync(LOCK_FILE);
105
+ return true;
106
+ } catch (e) {
107
+ try {
108
+ const stat = fs.statSync(LOCK_FILE);
109
+ if (Date.now() - stat.mtimeMs > 10000) {
110
+ fs.rmdirSync(LOCK_FILE);
111
+ continue;
112
+ }
113
+ } catch (e2) { /* lock was released between check */ }
114
+ const waitUntil = Date.now() + 50;
115
+ while (Date.now() < waitUntil) { /* busy wait */ }
116
+ }
117
+ }
118
+ return false;
119
+ }
120
+
121
+ function releaseLock() {
122
+ try { fs.rmdirSync(LOCK_FILE); } catch (e) { /* already released */ }
123
+ }
124
+
125
+ /**
126
+ * Remove stale agents from the stack (older than AGENT_STALENESS_MS)
127
+ * Also clears builderModeEnabledBy if that agent was removed
128
+ * Returns true if any agents were removed
129
+ */
130
+ function cleanupStaleAgents(stack) {
131
+ const now = Date.now();
132
+ const originalCount = stack.agents.length;
133
+
134
+ stack.agents = stack.agents.filter(agent => {
135
+ // Keep agents without timestamp (backward compatibility - assume current session)
136
+ if (!agent.startedAt) return true;
137
+
138
+ const agentTime = new Date(agent.startedAt).getTime();
139
+
140
+ // Remove agents with invalid timestamps (NaN from malformed dates)
141
+ if (isNaN(agentTime)) {
142
+ console.error(`[builder-mode-manager] Warning: Invalid timestamp for agent ${agent.type}, removing`);
143
+ return false;
144
+ }
145
+
146
+ return (now - agentTime) < AGENT_STALENESS_MS;
147
+ });
148
+
149
+ const removed = originalCount - stack.agents.length;
150
+ if (removed > 0) {
151
+ console.error(`[builder-mode-manager] Cleaned up ${removed} stale agent(s) from stack`);
152
+ }
153
+
154
+ // Clear builderModeEnabledBy if that agent is no longer in stack OR stack is empty
155
+ if (stack.builderModeEnabledBy) {
156
+ const enablerStillActive = stack.agents.some(a => a.type === stack.builderModeEnabledBy);
157
+ if (!enablerStillActive || stack.agents.length === 0) {
158
+ stack.builderModeEnabledBy = null;
159
+ }
160
+ }
161
+
162
+ return removed > 0;
163
+ }
164
+
73
165
  /**
74
166
  * Load the agent stack (tracks nested agent calls)
167
+ * Always reads fresh from file - no caching to avoid race conditions
168
+ * Automatically cleans up stale agents from previous sessions
75
169
  */
76
170
  function loadAgentStack() {
77
171
  try {
78
172
  if (fs.existsSync(AGENT_STACK_FILE)) {
79
- return JSON.parse(fs.readFileSync(AGENT_STACK_FILE, 'utf8'));
173
+ const stack = JSON.parse(fs.readFileSync(AGENT_STACK_FILE, 'utf8'));
174
+
175
+ // Clean up stale agents from previous sessions
176
+ const hadStaleAgents = cleanupStaleAgents(stack);
177
+
178
+ // If we cleaned up agents, save the updated stack
179
+ if (hadStaleAgents) {
180
+ // Also reset builder mode if no agents remain
181
+ if (stack.agents.length === 0) {
182
+ stack.builderModeEnabledBy = null;
183
+ disableBuilderMode();
184
+ }
185
+ saveAgentStack(stack);
186
+ }
187
+
188
+ return stack;
80
189
  }
81
- } catch {}
190
+ } catch (e) {
191
+ console.error(`[builder-mode-manager] Warning: ${e.message}`);
192
+ }
193
+
82
194
  return { agents: [], builderModeEnabledBy: null };
83
195
  }
84
196
 
85
197
  /**
86
- * Save the agent stack
198
+ * Save the agent stack atomically (write to temp, then rename)
87
199
  */
88
200
  function saveAgentStack(stack) {
89
- fs.writeFileSync(AGENT_STACK_FILE, JSON.stringify(stack, null, 2));
201
+ const tmpFile = AGENT_STACK_FILE + '.tmp-' + process.pid;
202
+ fs.writeFileSync(tmpFile, JSON.stringify(stack, null, 2), { mode: 0o600 });
203
+ fs.renameSync(tmpFile, AGENT_STACK_FILE); // Atomic on POSIX
90
204
  }
91
205
 
92
206
  /**
93
- * Enable builder mode
207
+ * Enable builder mode (atomic write: temp file + rename)
94
208
  */
95
209
  function enableBuilderMode(agentName) {
96
- fs.writeFileSync(BUILDER_MODE_FILE, JSON.stringify({
210
+ const tmpFile = BUILDER_MODE_FILE + '.tmp-' + process.pid;
211
+ fs.writeFileSync(tmpFile, JSON.stringify({
97
212
  enabledAt: new Date().toISOString(),
98
213
  enabledBy: agentName
99
- }));
214
+ }), { mode: 0o600 });
215
+ fs.renameSync(tmpFile, BUILDER_MODE_FILE); // Atomic on POSIX
100
216
  }
101
217
 
102
218
  /**
@@ -118,7 +234,7 @@ function isBuilderModeActive() {
118
234
  }
119
235
 
120
236
  function processHook(data) {
121
- const { tool_name, tool_input, tool_response, hook_type } = data;
237
+ const { tool_name, tool_input, tool_response } = data;
122
238
 
123
239
  // Only handle Task tool
124
240
  if (tool_name !== 'Task') {
@@ -133,48 +249,61 @@ function processHook(data) {
133
249
  process.exit(0);
134
250
  }
135
251
 
136
- const needsBuilderMode = CODE_EDITING_AGENTS.includes(agentType);
137
- const stack = loadAgentStack();
252
+ const needsBuilderMode = CODE_EDITING_AGENTS.some(name => agentType.includes(name));
138
253
 
139
254
  // Determine if this is PreToolUse or PostToolUse based on presence of tool_response
140
255
  const isPreToolUse = tool_response === undefined;
141
256
 
142
- if (isPreToolUse) {
143
- // === PreToolUse: Agent is about to be spawned ===
144
-
145
- // Push agent onto stack
146
- stack.agents.push({
147
- type: agentType,
148
- startedAt: new Date().toISOString(),
149
- needsBuilderMode
150
- });
151
-
152
- // Enable builder mode if needed and not already active
153
- if (needsBuilderMode && !isBuilderModeActive()) {
154
- enableBuilderMode(agentType);
155
- stack.builderModeEnabledBy = agentType;
156
- console.error(`🔧 Builder mode AUTO-ENABLED for agent: ${agentType}`);
157
- }
158
-
159
- saveAgentStack(stack);
160
-
161
- } else {
162
- // === PostToolUse: Agent has completed ===
163
-
164
- // Pop the most recent agent from stack
165
- const completedAgent = stack.agents.pop();
166
-
167
- // Check if any remaining agents need builder mode
168
- const remainingNeedBuilderMode = stack.agents.some(a => a.needsBuilderMode);
169
-
170
- // Disable builder mode if no more agents need it
171
- if (isBuilderModeActive() && !remainingNeedBuilderMode) {
172
- disableBuilderMode();
173
- stack.builderModeEnabledBy = null;
174
- console.error(`🔒 Builder mode AUTO-DISABLED (${agentType} completed)`);
257
+ const locked = acquireLock();
258
+ try {
259
+ const stack = loadAgentStack();
260
+
261
+ if (isPreToolUse) {
262
+ // === PreToolUse: Agent is about to be spawned ===
263
+
264
+ // Push agent onto stack
265
+ stack.agents.push({
266
+ type: agentType,
267
+ startedAt: new Date().toISOString(),
268
+ needsBuilderMode
269
+ });
270
+
271
+ // Enable builder mode if needed and not already active
272
+ if (needsBuilderMode && !isBuilderModeActive()) {
273
+ enableBuilderMode(agentType);
274
+ stack.builderModeEnabledBy = agentType;
275
+ console.error(`🔧 Builder mode AUTO-ENABLED for agent: ${agentType}`);
276
+ }
277
+
278
+ saveAgentStack(stack);
279
+
280
+ } else {
281
+ // === PostToolUse: Agent has completed ===
282
+
283
+ // Remove the matching agent from stack (find last one with this type)
284
+ // Using findLastIndex instead of pop() to handle parallel agents completing out of order
285
+ const removeIdx = stack.agents.findLastIndex(a => a.type === agentType);
286
+ if (removeIdx >= 0) {
287
+ stack.agents.splice(removeIdx, 1);
288
+ } else {
289
+ // Agent not found - could be stale or already cleaned up
290
+ console.error(`[builder-mode-manager] Warning: agent ${agentType} not found in stack, skipping removal`);
291
+ }
292
+
293
+ // Check if any remaining agents need builder mode
294
+ const remainingNeedBuilderMode = stack.agents.some(a => a.needsBuilderMode);
295
+
296
+ // Disable builder mode if no more agents need it
297
+ if (isBuilderModeActive() && !remainingNeedBuilderMode) {
298
+ disableBuilderMode();
299
+ stack.builderModeEnabledBy = null;
300
+ console.error(`🔒 Builder mode AUTO-DISABLED (${agentType} completed)`);
301
+ }
302
+
303
+ saveAgentStack(stack);
175
304
  }
176
-
177
- saveAgentStack(stack);
305
+ } finally {
306
+ if (locked) releaseLock();
178
307
  }
179
308
 
180
309
  // Always allow Task tool
@@ -0,0 +1,283 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * <hook-name>bulk-activity-guard</hook-name>
4
+ *
5
+ * <purpose>
6
+ * Confirms before creating many activities at once.
7
+ * Prevents accidental bulk data creation via Dmitri agent.
8
+ * </purpose>
9
+ *
10
+ * <triggers>
11
+ * - PreToolUse on mcp__hailer__create_activity
12
+ * - PreToolUse on mcp__hailer__bulk_create_activities (if exists)
13
+ * </triggers>
14
+ *
15
+ * <thresholds>
16
+ * - Single call with array > 5 items: Warn
17
+ * - Tracked calls in short window > 10: Warn
18
+ * </thresholds>
19
+ *
20
+ * <behavior>
21
+ * 1. Check if bulk creation (array input or rapid sequential calls)
22
+ * 2. If threshold exceeded, block and require confirmation
23
+ * 3. Track recent calls in temp file for sequential detection
24
+ * </behavior>
25
+ */
26
+
27
+ const fs = require('fs');
28
+ const path = require('path');
29
+ const os = require('os');
30
+
31
+ // Skip in yolo mode
32
+ try {
33
+ const statePath = path.join(process.env.CLAUDE_PROJECT_DIR || process.cwd(), '.claude', 'yolo-state.json');
34
+ const state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
35
+ if (state.mode === 'yolo') process.exit(0);
36
+ } catch (e) {
37
+ // ENOENT is expected when not in yolo mode - only warn on unexpected errors
38
+ if (e.code !== 'ENOENT' && !e.message.includes('Unexpected')) {
39
+ console.error(`[bulk-activity-guard] Warning: ${e.message}`);
40
+ }
41
+ }
42
+
43
+ // Skip in subagent context - orchestrator handles confirmation via delegation-bulk-guard
44
+ // Subagents can't use AskUserQuestion or Bash, so blocking them is unrecoverable
45
+ if (process.env.CLAUDE_AGENT_ID || process.env.CLAUDE_SUBAGENT) {
46
+ // Output allow and exit - orchestrator already confirmed
47
+ console.log(JSON.stringify({ decision: 'allow' }));
48
+ process.exit(0);
49
+ }
50
+
51
+ const TRACKER_FILE = path.join(os.tmpdir(), '.claude-activity-creation-tracker.json');
52
+ const LOCK_FILE = TRACKER_FILE + '.lock';
53
+ const LOCK_TIMEOUT = 5000; // 5 seconds max wait
54
+ const BULK_THRESHOLD = 5;
55
+ const SEQUENTIAL_THRESHOLD = 10;
56
+ const TIME_WINDOW_MS = 60000; // 1 minute window for sequential detection
57
+
58
+ // Read hook input from stdin
59
+ let input = '';
60
+ process.stdin.setEncoding('utf8');
61
+ process.stdin.on('data', chunk => input += chunk);
62
+ process.stdin.on('end', () => {
63
+ try {
64
+ const data = JSON.parse(input);
65
+ processHook(data);
66
+ } catch (e) {
67
+ console.error(`[bulk-activity-guard] Warning: ${e.message}`);
68
+ console.log(JSON.stringify({ decision: 'allow' }));
69
+ process.exit(0);
70
+ }
71
+ });
72
+
73
+ function outputAllow() {
74
+ console.log(JSON.stringify({ decision: 'allow' }));
75
+ process.exit(0);
76
+ }
77
+
78
+ function outputBlock(message) {
79
+ console.log(JSON.stringify({
80
+ decision: 'block',
81
+ reason: message
82
+ }));
83
+ process.exit(0);
84
+ }
85
+
86
+ function acquireLock() {
87
+ const start = Date.now();
88
+ while (Date.now() - start < LOCK_TIMEOUT) {
89
+ try {
90
+ fs.mkdirSync(LOCK_FILE);
91
+ return true;
92
+ } catch (e) {
93
+ // Lock exists - check if stale (older than 10 seconds)
94
+ try {
95
+ const stat = fs.statSync(LOCK_FILE);
96
+ if (Date.now() - stat.mtimeMs > 10000) {
97
+ fs.rmdirSync(LOCK_FILE);
98
+ continue;
99
+ }
100
+ } catch (e2) { /* lock was released between check */ }
101
+ // Wait 50ms and retry
102
+ const waitUntil = Date.now() + 50;
103
+ while (Date.now() < waitUntil) { /* busy wait - hooks are sync */ }
104
+ }
105
+ }
106
+ return false; // Timeout - proceed without lock (fallback)
107
+ }
108
+
109
+ function releaseLock() {
110
+ try { fs.rmdirSync(LOCK_FILE); } catch (e) { /* already released */ }
111
+ }
112
+
113
+ function loadTracker() {
114
+ try {
115
+ if (fs.existsSync(TRACKER_FILE)) {
116
+ return JSON.parse(fs.readFileSync(TRACKER_FILE, 'utf8'));
117
+ }
118
+ } catch (e) {
119
+ console.error(`[bulk-activity-guard] Warning: ${e.message}`);
120
+ }
121
+ return { calls: [], confirmed: false, confirmedAt: null };
122
+ }
123
+
124
+ function saveTracker(tracker) {
125
+ // Atomic write: write to temp file, then rename
126
+ const tmpFile = TRACKER_FILE + '.tmp-' + process.pid;
127
+ fs.writeFileSync(tmpFile, JSON.stringify(tracker, null, 2), { mode: 0o600 });
128
+ fs.renameSync(tmpFile, TRACKER_FILE); // Atomic on POSIX
129
+ }
130
+
131
+ function processHook(data) {
132
+ const { tool_name, tool_input } = data;
133
+
134
+ // Only check activity creation tools - exact match to avoid false positives
135
+ if (!tool_name || (tool_name !== 'mcp__hailer__create_activity' && tool_name !== 'mcp__hailer__bulk_create_activities')) {
136
+ outputAllow();
137
+ return;
138
+ }
139
+
140
+ const locked = acquireLock();
141
+ try {
142
+ const now = Date.now();
143
+ const tracker = loadTracker();
144
+
145
+ // Clean old calls outside time window
146
+ tracker.calls = tracker.calls.filter(t => now - t < TIME_WINDOW_MS);
147
+
148
+ // Check if recently confirmed (within last 5 minutes)
149
+ if (tracker.confirmed && tracker.confirmedAt && (now - tracker.confirmedAt) < 300000) {
150
+ // User recently confirmed bulk creation, allow
151
+ tracker.calls.push(now);
152
+ saveTracker(tracker);
153
+ outputAllow();
154
+ return;
155
+ }
156
+
157
+ // Reset confirmation if window expired
158
+ tracker.confirmed = false;
159
+ tracker.confirmedAt = null;
160
+
161
+ // Check for bulk array in input
162
+ let itemCount = 1;
163
+ try {
164
+ if (tool_input?.activities && Array.isArray(tool_input.activities)) {
165
+ itemCount = tool_input.activities.length;
166
+ }
167
+ } catch {}
168
+
169
+ // Add current call
170
+ tracker.calls.push(now);
171
+ const recentCallCount = tracker.calls.length;
172
+ saveTracker(tracker);
173
+
174
+ // Check thresholds
175
+ const isBulkArray = itemCount > BULK_THRESHOLD;
176
+ const isSequentialBulk = recentCallCount > SEQUENTIAL_THRESHOLD;
177
+
178
+ if (isBulkArray || isSequentialBulk) {
179
+ const reason = isBulkArray
180
+ ? `Creating ${itemCount} activities in one call`
181
+ : `${recentCallCount} activity creations in the last minute`;
182
+
183
+ outputBlock(`
184
+ 🚫 BLOCKED: Bulk Activity Creation Detected
185
+
186
+ **Reason:** ${reason}
187
+
188
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
189
+ CONFIRM WITH USER FIRST
190
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
191
+
192
+ Use AskUserQuestion to confirm:
193
+
194
+ \`\`\`json
195
+ {
196
+ "questions": [{
197
+ "question": "About to create ${isBulkArray ? itemCount : recentCallCount}+ activities. Continue?",
198
+ "header": "Bulk Create",
199
+ "options": [
200
+ { "label": "Yes, create all", "description": "Proceed with bulk creation" },
201
+ { "label": "No, stop", "description": "Cancel the operation" }
202
+ ],
203
+ "multiSelect": false
204
+ }]
205
+ }
206
+ \`\`\`
207
+
208
+ If user confirms, run this to unlock for 5 minutes:
209
+ Bash: node "${process.argv[1].replace(/'/g, "'\\''")}" --confirm
210
+
211
+ Then retry the operation.
212
+
213
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
214
+ `);
215
+ }
216
+
217
+ outputAllow();
218
+ } finally {
219
+ if (locked) releaseLock();
220
+ }
221
+ }
222
+
223
+ // CLI: Confirm bulk creation
224
+ if (process.argv[2] === '--confirm') {
225
+ const locked = acquireLock();
226
+ try {
227
+ const tracker = loadTracker();
228
+ tracker.confirmed = true;
229
+ tracker.confirmedAt = Date.now();
230
+ saveTracker(tracker);
231
+ console.log('✅ Bulk activity creation confirmed for 5 minutes');
232
+ } finally {
233
+ if (locked) releaseLock();
234
+ }
235
+ process.exit(0);
236
+ }
237
+
238
+ // CLI: Reset tracker
239
+ if (process.argv[2] === '--reset') {
240
+ if (fs.existsSync(TRACKER_FILE)) {
241
+ fs.unlinkSync(TRACKER_FILE);
242
+ }
243
+ console.log('✅ Activity creation tracker reset');
244
+ process.exit(0);
245
+ }
246
+
247
+ // CLI: Status
248
+ if (process.argv[2] === '--status') {
249
+ const tracker = loadTracker();
250
+ const now = Date.now();
251
+ const recentCalls = tracker.calls.filter(t => now - t < TIME_WINDOW_MS).length;
252
+ console.log('Activity Creation Tracker');
253
+ console.log('=========================');
254
+ console.log(`Recent calls (1 min): ${recentCalls}`);
255
+ console.log(`Confirmed: ${tracker.confirmed}`);
256
+ if (tracker.confirmedAt) {
257
+ const remaining = Math.max(0, 300000 - (now - tracker.confirmedAt));
258
+ console.log(`Confirmation expires in: ${Math.round(remaining / 1000)}s`);
259
+ }
260
+ process.exit(0);
261
+ }
262
+
263
+ // CLI: Help
264
+ if (process.argv[2] === '--help' || process.argv[2] === '-h') {
265
+ console.log(`
266
+ Bulk Activity Guard - Confirms before bulk activity creation
267
+
268
+ Usage:
269
+ node bulk-activity-guard.cjs --confirm Confirm bulk creation for 5 minutes
270
+ node bulk-activity-guard.cjs --reset Reset the tracker
271
+ node bulk-activity-guard.cjs --status Show current status
272
+ node bulk-activity-guard.cjs --help Show this help
273
+
274
+ Thresholds:
275
+ - Single call with >${BULK_THRESHOLD} items: Blocked
276
+ - >${SEQUENTIAL_THRESHOLD} calls in 1 minute: Blocked
277
+
278
+ As a hook:
279
+ Reads JSON from stdin with tool_name and tool_input
280
+ Outputs JSON with decision: "allow" or "block"
281
+ `);
282
+ process.exit(0);
283
+ }