create-walle 0.9.7 → 0.9.9

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 (146) hide show
  1. package/README.md +1 -1
  2. package/bin/create-walle.js +32 -1
  3. package/bin/mcp-inject.js +60 -0
  4. package/package.json +11 -3
  5. package/template/bin/setup.js +2 -1
  6. package/template/claude-code-skill.md +35 -34
  7. package/template/claude-task-manager/api-prompts.js +113 -70
  8. package/template/claude-task-manager/approval-agent.js +127 -22
  9. package/template/claude-task-manager/atomic-write.js +22 -0
  10. package/template/claude-task-manager/db.js +515 -28
  11. package/template/claude-task-manager/git-utils.js +112 -1
  12. package/template/claude-task-manager/public/css/setup.css +191 -0
  13. package/template/claude-task-manager/public/css/walle-session.css +3 -0
  14. package/template/claude-task-manager/public/css/walle.css +305 -0
  15. package/template/claude-task-manager/public/index.html +3174 -296
  16. package/template/claude-task-manager/public/js/setup.js +698 -0
  17. package/template/claude-task-manager/public/js/walle-session.js +124 -1
  18. package/template/claude-task-manager/public/js/walle.js +1420 -42
  19. package/template/claude-task-manager/public/setup.html +693 -104
  20. package/template/claude-task-manager/server-state.js +8 -1
  21. package/template/claude-task-manager/server.js +2280 -227
  22. package/template/claude-task-manager/session-utils.js +50 -3
  23. package/template/claude-task-manager/workers/harvest-worker.js +36 -0
  24. package/template/claude-task-manager/workers/scrollback-worker.js +60 -0
  25. package/template/claude-task-manager/workers/vterm-worker.js +179 -0
  26. package/template/package.json +2 -2
  27. package/template/wall-e/agent.js +288 -146
  28. package/template/wall-e/api-walle.js +588 -4
  29. package/template/wall-e/brain.js +565 -10
  30. package/template/wall-e/chat.js +298 -51
  31. package/template/wall-e/coding-orchestrator.js +31 -0
  32. package/template/wall-e/context/compactor.js +21 -0
  33. package/template/wall-e/context/context-builder.js +216 -64
  34. package/template/wall-e/context/topic-matcher.js +40 -11
  35. package/template/wall-e/docs/prompt-architecture.md +121 -0
  36. package/template/wall-e/embeddings.js +810 -0
  37. package/template/wall-e/eval/aggregator.js +115 -0
  38. package/template/wall-e/eval/benchmarks/chat-eval.json +1041 -0
  39. package/template/wall-e/eval/benchmarks/chat.json +82 -0
  40. package/template/wall-e/eval/benchmarks/coding.json +122 -0
  41. package/template/wall-e/eval/benchmarks/memory-retrieval.json +82 -0
  42. package/template/wall-e/eval/benchmarks/reasoning.json +82 -0
  43. package/template/wall-e/eval/benchmarks.js +464 -0
  44. package/template/wall-e/eval/chat-eval.js +509 -0
  45. package/template/wall-e/eval/evaluate.js +202 -0
  46. package/template/wall-e/eval/evaluator.js +373 -0
  47. package/template/wall-e/eval/exporter.js +212 -0
  48. package/template/wall-e/eval/harvester.js +472 -0
  49. package/template/wall-e/eval/head-to-head.js +337 -0
  50. package/template/wall-e/eval/promoter.js +146 -0
  51. package/template/wall-e/eval/replay.js +381 -0
  52. package/template/wall-e/eval/shadow.js +144 -0
  53. package/template/wall-e/eval/train.py +320 -0
  54. package/template/wall-e/eval/trainer.js +232 -0
  55. package/template/wall-e/evaluation/complexity.js +3 -0
  56. package/template/wall-e/evaluation/index.js +1 -1
  57. package/template/wall-e/evaluation/learner.js +14 -2
  58. package/template/wall-e/evaluation/quorum-evaluator.js +544 -0
  59. package/template/wall-e/evaluation/router.js +237 -29
  60. package/template/wall-e/evaluation/scorecard.js +74 -2
  61. package/template/wall-e/evaluation/self-critique.js +188 -0
  62. package/template/wall-e/evaluation/tier-selector.js +166 -0
  63. package/template/wall-e/evaluation/user-signals.js +109 -0
  64. package/template/wall-e/extraction/knowledge-extractor.js +60 -17
  65. package/template/wall-e/lib/scheduler.js +389 -0
  66. package/template/wall-e/llm/client.js +74 -6
  67. package/template/wall-e/llm/index.js +1 -0
  68. package/template/wall-e/llm/mlx.js +220 -0
  69. package/template/wall-e/llm/ollama-setup.js +228 -0
  70. package/template/wall-e/llm/ollama.js +20 -3
  71. package/template/wall-e/llm/provider-availability.js +213 -0
  72. package/template/wall-e/llm/provider-detector.js +273 -0
  73. package/template/wall-e/loops/backfill.js +338 -0
  74. package/template/wall-e/loops/initiative.js +13 -0
  75. package/template/wall-e/loops/reflect.js +13 -0
  76. package/template/wall-e/loops/tasks.js +56 -11
  77. package/template/wall-e/loops/think.js +55 -4
  78. package/template/wall-e/mcp-server.js +235 -0
  79. package/template/wall-e/package.json +1 -0
  80. package/template/wall-e/scripts/eval-embeddings.js +311 -0
  81. package/template/wall-e/scripts/slack-channel-history.js +1 -1
  82. package/template/wall-e/server.js +17 -0
  83. package/template/wall-e/skills/_bundled/coding-agent/run.js +9 -1
  84. package/template/wall-e/skills/_bundled/email-sync/run.js +9 -1
  85. package/template/wall-e/skills/_bundled/file-ingest/run.js +6 -1
  86. package/template/wall-e/skills/_bundled/glean-team-sync/run.js +9 -4
  87. package/template/wall-e/skills/_bundled/google-calendar/run.js +40 -6
  88. package/template/wall-e/skills/_bundled/mcp-scan/run.js +9 -4
  89. package/template/wall-e/skills/_bundled/model-trainer/SKILL.md +1 -1
  90. package/template/wall-e/skills/_bundled/morning-briefing/run.js +82 -6
  91. package/template/wall-e/skills/_bundled/proactive-alerts/run.js +9 -4
  92. package/template/wall-e/skills/_bundled/slack-mentions/run.js +24 -17
  93. package/template/wall-e/skills/_bundled/training-harvester/SKILL.md +43 -0
  94. package/template/wall-e/skills/_bundled/training-harvester/run.js +46 -0
  95. package/template/wall-e/skills/skill-planner.js +92 -64
  96. package/template/wall-e/telemetry.js +65 -1
  97. package/template/wall-e/test/eval/aggregator.test.js +180 -0
  98. package/template/wall-e/test/eval/benchmarks.test.js +373 -0
  99. package/template/wall-e/test/eval/brain-shadow.test.js +359 -0
  100. package/template/wall-e/test/eval/evaluate.test.js +221 -0
  101. package/template/wall-e/test/eval/evaluator.test.js +340 -0
  102. package/template/wall-e/test/eval/exporter.test.js +353 -0
  103. package/template/wall-e/test/eval/harvester.test.js +718 -0
  104. package/template/wall-e/test/eval/head-to-head.test.js +485 -0
  105. package/template/wall-e/test/eval/promoter.test.js +175 -0
  106. package/template/wall-e/test/eval/shadow.test.js +156 -0
  107. package/template/wall-e/test/eval/trainer.test.js +314 -0
  108. package/template/wall-e/test/evaluation/scorecard.test.js +33 -0
  109. package/template/wall-e/test/llm/client.test.js +74 -6
  110. package/template/wall-e/test/training/aggregator.test.js +180 -0
  111. package/template/wall-e/test/training/brain-shadow.test.js +359 -0
  112. package/template/wall-e/test/training/evaluator.test.js +340 -0
  113. package/template/wall-e/test/training/exporter.test.js +141 -1
  114. package/template/wall-e/test/training/harvester.test.js +718 -0
  115. package/template/wall-e/test/training/promoter.test.js +175 -0
  116. package/template/wall-e/test/training/shadow.test.js +156 -0
  117. package/template/wall-e/test/training/trainer.test.js +77 -4
  118. package/template/wall-e/tests/backfill.test.js +126 -0
  119. package/template/wall-e/tests/brain.test.js +4 -4
  120. package/template/wall-e/tests/coding-orchestrator.test.js +212 -217
  121. package/template/wall-e/tests/context-builder.test.js +42 -35
  122. package/template/wall-e/tests/embeddings.test.js +378 -0
  123. package/template/wall-e/tests/mcp-inject.test.js +68 -0
  124. package/template/wall-e/tests/mcp-server.test.js +219 -0
  125. package/template/wall-e/tests/ollama-setup.test.js +81 -0
  126. package/template/wall-e/tests/provider-availability.test.js +231 -0
  127. package/template/wall-e/tests/provider-detector.test.js +127 -0
  128. package/template/wall-e/tests/quorum-evaluator.test.js +277 -0
  129. package/template/wall-e/tests/scheduler.test.js +546 -0
  130. package/template/wall-e/tests/scorecard-evolution.test.js +96 -0
  131. package/template/wall-e/tests/self-critique.test.js +162 -0
  132. package/template/wall-e/tests/think.test.js +34 -15
  133. package/template/wall-e/tests/tier-selector.test.js +109 -0
  134. package/template/wall-e/tests/user-signals.test.js +73 -0
  135. package/template/wall-e/tools/local-tools.js +143 -14
  136. package/template/wall-e/tools/slack-mcp.js +20 -16
  137. package/template/wall-e/training/aggregator.js +115 -0
  138. package/template/wall-e/training/evaluator.js +373 -0
  139. package/template/wall-e/training/exporter.js +86 -1
  140. package/template/wall-e/training/harvester.js +476 -0
  141. package/template/wall-e/training/promoter.js +146 -0
  142. package/template/wall-e/training/replay.js +381 -0
  143. package/template/wall-e/training/shadow.js +144 -0
  144. package/template/wall-e/training/train.py +85 -34
  145. package/template/wall-e/training/trainer.js +35 -5
  146. package/template/wall-e/evaluation/quorum.js +0 -256
@@ -14,6 +14,15 @@ const dbModule = require('./db');
14
14
  const PROCEED_PATTERN = /Do you want to (proceed|make this edit to .+|create .+|overwrite .+)\??/;
15
15
  const YES_NO_PATTERN = /[>❯]\s*1\.\s*Yes/;
16
16
 
17
+ // Delay (ms) before sending the auto-approve keystroke. Lower = faster response.
18
+ const APPROVE_DELAY_MS = 100;
19
+ const ENTER_DELAY_MS = 30;
20
+
21
+ // Determine which option to send: "2" for "Yes, allow all" when available, "1" for plain "Yes"
22
+ function getApproveKeystroke(context) {
23
+ return context.hasAllowAll ? '2' : '1';
24
+ }
25
+
17
26
  // Parse the terminal buffer to extract the approval context
18
27
  function parseApprovalContext(cleanText) {
19
28
  const lines = cleanText.split('\n').map(l => l.trim()).filter(Boolean);
@@ -78,14 +87,61 @@ function parseApprovalContext(cleanText) {
78
87
  const ctxEnd = Math.min(lines.length, proceedIdx + 5); // include Yes/No options
79
88
  const fullContext = lines.slice(ctxStart, ctxEnd).join('\n');
80
89
 
90
+ // Detect if "2. Yes, allow all..." option is available (Edit/Write prompts)
91
+ let hasAllowAll = false;
92
+ for (let i = proceedIdx + 1; i < Math.min(proceedIdx + 8, lines.length); i++) {
93
+ if (/2\.\s*Yes,\s*allow all/i.test(lines[i])) { hasAllowAll = true; break; }
94
+ }
95
+
81
96
  return {
82
97
  toolName: toolName || 'Unknown',
83
98
  command: command.slice(0, 2000),
84
99
  warning: warning || '',
85
100
  fullContext: fullContext.slice(0, 2000),
101
+ hasAllowAll,
86
102
  };
87
103
  }
88
104
 
105
+ // Normalize a command into a stable "signature" by extracting the command structure
106
+ // and replacing variable parts (paths, strings, numbers) with placeholders.
107
+ // Examples:
108
+ // "node -e 'console.log(1+2)'" → "node -e <arg>"
109
+ // "git commit -m 'fix typo'" → "git commit -m <arg>"
110
+ // "cat /Users/bob/ws/foo/bar.js" → "cat <path>"
111
+ // "python3 -c 'import json; ...'" → "python3 -c <arg>"
112
+ // "ls -la /some/dir" → "ls -la <path>"
113
+ // "kill -9 12345" → "kill -9 <num>"
114
+ // "curl http://localhost:3000/api" → "curl <url>"
115
+ function normalizeCommandSignature(toolName, command) {
116
+ const tool = (toolName || '').replace(/^[⏺●\s]+/, '').trim();
117
+ const cmd = (command || '').trim();
118
+ if (!cmd) return tool.toLowerCase();
119
+
120
+ // For non-Bash tools (Edit, Write, Read, etc.), signature is just the tool name
121
+ if (!/bash/i.test(tool)) return tool.toLowerCase();
122
+
123
+ // Extract the shell command — take first line, strip leading whitespace
124
+ const firstLine = cmd.split('\n')[0].trim();
125
+
126
+ let sig = firstLine
127
+ // Replace quoted strings (single/double/backtick) with <arg>
128
+ .replace(/(["'`])(?:(?!\1).)*\1/g, '<arg>')
129
+ // Replace URLs with <url>
130
+ .replace(/https?:\/\/\S+/g, '<url>')
131
+ // Replace absolute paths with <path>
132
+ .replace(/(?:\/[\w._-]+){2,}/g, '<path>')
133
+ // Replace standalone numbers (PIDs, ports, line numbers) with <num>
134
+ .replace(/\b\d{2,}\b/g, '<num>')
135
+ // Collapse multiple spaces
136
+ .replace(/\s+/g, ' ')
137
+ .trim();
138
+
139
+ // Limit length to prevent bloated signatures
140
+ if (sig.length > 150) sig = sig.slice(0, 150);
141
+
142
+ return sig.toLowerCase();
143
+ }
144
+
89
145
  // Reject regex patterns that could cause catastrophic backtracking (ReDoS).
90
146
  // Looks for nested quantifiers like (a+)+, (a*)*, (a+|b+)+ etc.
91
147
  function isSafeRegex(pattern) {
@@ -98,8 +154,20 @@ function isSafeRegex(pattern) {
98
154
  return true;
99
155
  }
100
156
 
101
- // Check if a learned rule already covers this situation
157
+ // Check if a learned rule already covers this situation.
158
+ // Two-tier matching: (1) exact signature lookup in DB (fast, O(1)),
159
+ // then (2) regex scan over all rules (slower, backwards-compatible).
102
160
  function findMatchingRule(context) {
161
+ // Tier 1: Signature-based lookup (indexed, instant)
162
+ const signature = normalizeCommandSignature(context.toolName, context.command);
163
+ if (signature) {
164
+ try {
165
+ const sigRule = dbModule.findApprovalRuleBySignature(signature);
166
+ if (sigRule) return sigRule;
167
+ } catch {}
168
+ }
169
+
170
+ // Tier 2: Regex-based scan (backwards-compatible with existing rules)
103
171
  let rules;
104
172
  try {
105
173
  rules = dbModule.listApprovalRules();
@@ -114,8 +182,8 @@ function findMatchingRule(context) {
114
182
  for (const rule of rules) {
115
183
  if (!rule.enabled) continue;
116
184
  try {
117
- if (!rule.pattern || rule.pattern.length > 200) continue; // skip overly complex patterns
118
- if (!isSafeRegex(rule.pattern)) continue; // skip ReDoS-prone patterns
185
+ if (!rule.pattern || rule.pattern.length > 200) continue;
186
+ if (!isSafeRegex(rule.pattern)) continue;
119
187
  const re = new RegExp(rule.pattern, 'i');
120
188
  if (re.test(searchText) || re.test(cmdText) || re.test(warnText)) {
121
189
  return rule;
@@ -172,6 +240,14 @@ function reviewWithHeuristics(context) {
172
240
  { re: /touch\s/, label: 'Create empty file', desc: 'Create or update file timestamps' },
173
241
  { re: /cp\s/, label: 'Copy files', desc: 'Copy files or directories' },
174
242
  { re: /mv\s/, label: 'Move/rename files', desc: 'Move or rename files' },
243
+ { re: /curl\s+(-[sSwfkL]+\s+)*(https?:\/\/localhost|http:\/\/127\.0\.0\.1)/, label: 'Curl localhost (GET)', desc: 'Read-only HTTP requests to local dev servers' },
244
+ { re: /grep\s+-?[crn]/, label: 'Grep search', desc: 'Search file contents with grep' },
245
+ { re: /wc\s/, label: 'Word count', desc: 'Count lines/words/bytes' },
246
+ { re: /head\s|tail\s/, label: 'Read file head/tail', desc: 'View beginning or end of files' },
247
+ { re: /which\s|type\s/, label: 'Find command', desc: 'Locate commands in PATH' },
248
+ { re: /echo\s[^|>]+$/, label: 'Echo output', desc: 'Print text to stdout (no redirect/pipe)' },
249
+ { re: /find\s.*-name/, label: 'Find files', desc: 'Search for files by name' },
250
+ { re: /sort\s|uniq\s/, label: 'Sort/unique', desc: 'Sort or deduplicate output' },
175
251
  ];
176
252
  for (const { re, label, desc } of devSafe) {
177
253
  if (re.test(cmd)) {
@@ -181,8 +257,8 @@ function reviewWithHeuristics(context) {
181
257
  }
182
258
  }
183
259
 
184
- // Default: approve with medium risk (user can review in the decisions log)
185
- return { decision: 'approve', reasoning: 'No API key; auto-approved with medium risk (heuristic)', riskLevel: 'medium',
260
+ // Default: approve with medium risk — NOT auto-approved (sent to AI reviewer or held for user)
261
+ return { decision: 'approve', reasoning: 'Unrecognized command needs review', riskLevel: 'medium', fallback: true,
186
262
  ruleLabel: context.toolName || 'Unknown', rulePattern: '',
187
263
  ruleDescription: 'Auto-approved without AI review' };
188
264
  }
@@ -310,12 +386,15 @@ async function handleApprovalCheck(sessionId, session, cleanText, broadcastFn) {
310
386
  };
311
387
 
312
388
  // Record and execute
313
- try { dbModule.addApprovalDecision(decision); } catch (e) { console.error('[approval-agent] DB error:', e.message); }
389
+ try {
390
+ dbModule.addApprovalDecision(decision);
391
+ dbModule.incrementApprovalRuleMatch(matchingRule.id);
392
+ } catch (e) { console.error('[approval-agent] DB error:', e.message); }
314
393
 
315
- // Send "1" for Yes then Enter
394
+ // Send approval keystroke ("2" for allow-all when available, "1" for plain Yes)
316
395
  setTimeout(() => {
317
- session.ptyProcess.write('1');
318
- setTimeout(() => session.ptyProcess.write('\r'), 50);
396
+ session.ptyProcess.write(getApproveKeystroke(context));
397
+ setTimeout(() => session.ptyProcess.write('\r'), ENTER_DELAY_MS);
319
398
  // Notify clients
320
399
  broadcastFn(sessionId, session, {
321
400
  type: 'approval-decision',
@@ -326,15 +405,15 @@ async function handleApprovalCheck(sessionId, session, cleanText, broadcastFn) {
326
405
  reasoning: decision.reasoning,
327
406
  riskLevel: decision.riskLevel,
328
407
  });
329
- }, 400);
408
+ }, APPROVE_DELAY_MS);
330
409
 
331
410
  return true;
332
411
  }
333
412
 
334
413
  // No matching rule — check heuristics for obvious safe/dangerous patterns first
335
414
  const heuristic = reviewWithHeuristics(context);
336
- if (heuristic.riskLevel === 'low') {
337
- // Heuristic says it's safe auto-approve without AI call
415
+ if (heuristic.riskLevel === 'low' || (heuristic.riskLevel === 'medium' && heuristic.decision === 'approve' && !heuristic.fallback)) {
416
+ // Low risk or medium with explicit rule match: auto-approve without AI call
338
417
  const decision = {
339
418
  sessionId,
340
419
  toolName: context.toolName,
@@ -347,14 +426,34 @@ async function handleApprovalCheck(sessionId, session, cleanText, broadcastFn) {
347
426
  riskLevel: 'low',
348
427
  };
349
428
  try { dbModule.addApprovalDecision(decision); } catch (e) { console.error('[approval-agent] DB error:', e.message); }
429
+
430
+ // Learn signature from heuristic approval so future matches use fast DB path.
431
+ // Only write if no signature rule exists yet (avoid DB write on every approval).
432
+ const heuristicSig = normalizeCommandSignature(context.toolName, context.command);
433
+ if (heuristicSig && heuristic.rulePattern) {
434
+ try {
435
+ if (!dbModule.findApprovalRuleBySignature(heuristicSig)) {
436
+ dbModule.upsertApprovalRule({
437
+ pattern: heuristic.rulePattern,
438
+ label: heuristic.ruleLabel || context.toolName,
439
+ description: heuristic.ruleDescription || '',
440
+ category: context.toolName.toLowerCase().replace(/\s+/g, '-'),
441
+ riskLevel: 'low',
442
+ enabled: true,
443
+ commandSignature: heuristicSig,
444
+ });
445
+ }
446
+ } catch {}
447
+ }
448
+
350
449
  setTimeout(() => {
351
- session.ptyProcess.write('1');
352
- setTimeout(() => session.ptyProcess.write('\r'), 50);
450
+ session.ptyProcess.write(getApproveKeystroke(context));
451
+ setTimeout(() => session.ptyProcess.write('\r'), ENTER_DELAY_MS);
353
452
  broadcastFn(sessionId, session, {
354
453
  type: 'approval-decision', sessionId, decision: 'approved', decidedBy: 'heuristic',
355
454
  label: heuristic.ruleLabel || context.toolName, reasoning: heuristic.reasoning, riskLevel: 'low',
356
455
  });
357
- }, 400);
456
+ }, APPROVE_DELAY_MS);
358
457
  return true;
359
458
  }
360
459
  if (heuristic.riskLevel === 'high') {
@@ -401,23 +500,28 @@ async function handleApprovalCheck(sessionId, session, cleanText, broadcastFn) {
401
500
  try { decisionId = dbModule.addApprovalDecision(decision); } catch (e) { console.error('[approval-agent] DB error:', e.message); }
402
501
 
403
502
  if (review.decision === 'approve') {
404
- // Auto-approve and learn a new rule
405
- if (review.rulePattern && review.ruleLabel) {
503
+ // Auto-approve and learn a new rule with command signature for fast future matching
504
+ const signature = normalizeCommandSignature(context.toolName, context.command);
505
+ const rulePattern = review.rulePattern || signature || '';
506
+ const ruleLabel = review.ruleLabel || context.toolName || 'Unknown';
507
+ if (rulePattern) {
406
508
  try {
407
509
  dbModule.upsertApprovalRule({
408
- pattern: review.rulePattern,
409
- label: review.ruleLabel,
510
+ pattern: rulePattern,
511
+ label: ruleLabel,
410
512
  description: review.ruleDescription || '',
411
513
  category: context.toolName.toLowerCase().replace(/\s+/g, '-'),
412
514
  riskLevel: review.riskLevel || 'low',
413
515
  enabled: true,
516
+ commandSignature: signature,
414
517
  });
518
+ console.log(`[approval-agent] Learned rule: "${ruleLabel}" sig="${signature}" pattern="${rulePattern}"`);
415
519
  } catch (e) { console.error('[approval-agent] Rule save error:', e.message); }
416
520
  }
417
521
 
418
522
  setTimeout(() => {
419
- session.ptyProcess.write('1');
420
- setTimeout(() => session.ptyProcess.write('\r'), 50);
523
+ session.ptyProcess.write(getApproveKeystroke(context));
524
+ setTimeout(() => session.ptyProcess.write('\r'), ENTER_DELAY_MS);
421
525
  broadcastFn(sessionId, session, {
422
526
  type: 'approval-decision',
423
527
  sessionId,
@@ -427,7 +531,7 @@ async function handleApprovalCheck(sessionId, session, cleanText, broadcastFn) {
427
531
  reasoning: review.reasoning,
428
532
  riskLevel: review.riskLevel,
429
533
  });
430
- }, 400);
534
+ }, APPROVE_DELAY_MS);
431
535
  } else {
432
536
  // Escalate to user
433
537
  broadcastFn(sessionId, session, {
@@ -449,6 +553,7 @@ async function handleApprovalCheck(sessionId, session, cleanText, broadcastFn) {
449
553
 
450
554
  module.exports = {
451
555
  parseApprovalContext,
556
+ normalizeCommandSignature,
452
557
  findMatchingRule,
453
558
  reviewWithAI,
454
559
  handleApprovalCheck,
@@ -0,0 +1,22 @@
1
+ 'use strict';
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+
5
+ /**
6
+ * Atomic file write using tmp + rename (POSIX rename is atomic on same filesystem).
7
+ * Prevents read-modify-write races when multiple sessions write to .env concurrently.
8
+ */
9
+ function atomicWriteFileSync(filePath, data, options) {
10
+ const resolved = path.resolve(filePath);
11
+ const tmp = resolved + '.tmp.' + process.pid + '.' + Date.now();
12
+ try {
13
+ fs.writeFileSync(tmp, data, options);
14
+ fs.renameSync(tmp, resolved);
15
+ } catch (e) {
16
+ // Clean up tmp file on failure
17
+ try { fs.unlinkSync(tmp); } catch {}
18
+ throw e;
19
+ }
20
+ }
21
+
22
+ module.exports = { atomicWriteFileSync };