moflo 4.12.4 → 4.12.5

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 (108) hide show
  1. package/.claude/guidance/shipped/moflo-agent-rules.md +1 -0
  2. package/.claude/guidance/shipped/moflo-claude-swarm-cohesion.md +1 -1
  3. package/.claude/guidance/shipped/moflo-cli-reference.md +4 -4
  4. package/.claude/guidance/shipped/moflo-core-guidance.md +4 -4
  5. package/.claude/guidance/shipped/moflo-guidance-rules.md +21 -0
  6. package/.claude/guidance/shipped/moflo-inline-fixes.md +98 -0
  7. package/.claude/guidance/shipped/moflo-root-cause-discipline.md +10 -10
  8. package/.claude/guidance/shipped/moflo-sdd.md +5 -5
  9. package/.claude/guidance/shipped/moflo-spell-engine.md +1 -1
  10. package/.claude/guidance/shipped/moflo-yaml-reference.md +8 -8
  11. package/.claude/helpers/gate.cjs +46 -7
  12. package/.claude/helpers/pr-create-command.cjs +440 -0
  13. package/.claude/skills/eldar/SKILL.md +1 -1
  14. package/.claude/skills/fl/SKILL.md +6 -6
  15. package/.claude/skills/fl/execution-modes.md +3 -3
  16. package/.claude/skills/fl/phases.md +8 -8
  17. package/.claude/skills/fl/sdd.md +6 -6
  18. package/.claude/skills/guidance/SKILL.md +3 -0
  19. package/.claude/skills/verify/SKILL.md +4 -4
  20. package/bin/gate.cjs +46 -7
  21. package/bin/hooks.mjs +4 -1
  22. package/bin/lib/retired-files.mjs +38 -6
  23. package/bin/lib/shipped-scripts.json +2 -1
  24. package/bin/lib/skill-categories.mjs +81 -17
  25. package/bin/pr-create-command.cjs +440 -0
  26. package/bin/session-start-launcher.mjs +11 -6
  27. package/dist/src/cli/aidefence/domain/entities/threat.js +2 -1
  28. package/dist/src/cli/commands/doctor-checks-deep.js +3 -2
  29. package/dist/src/cli/commands/doctor-checks-memory-access.js +3 -2
  30. package/dist/src/cli/commands/doctor-checks-swarm.js +3 -2
  31. package/dist/src/cli/commands/hooks.js +2 -1
  32. package/dist/src/cli/commands/mcp.js +3 -2
  33. package/dist/src/cli/commands/memory.js +2 -1
  34. package/dist/src/cli/commands/performance.js +2 -1
  35. package/dist/src/cli/commands/retire.js +1 -1
  36. package/dist/src/cli/commands/spell-schedule.js +2 -1
  37. package/dist/src/cli/commands/swarm.js +216 -104
  38. package/dist/src/cli/guidance/headless.js +5 -8
  39. package/dist/src/cli/guidance/retriever.js +27 -6
  40. package/dist/src/cli/guidance/ruvbot-integration.js +2 -1
  41. package/dist/src/cli/guidance/uncertainty.js +2 -1
  42. package/dist/src/cli/hooks/bridge/official-hooks-bridge.js +2 -1
  43. package/dist/src/cli/hooks/mcp/index.js +3 -2
  44. package/dist/src/cli/hooks/reasoningbank/index.js +2 -1
  45. package/dist/src/cli/hooks/registry/index.js +2 -1
  46. package/dist/src/cli/hooks/swarm/index.js +6 -5
  47. package/dist/src/cli/init/claudemd-generator.js +2 -1
  48. package/dist/src/cli/init/helpers-generator.js +1 -1
  49. package/dist/src/cli/init/moflo-yaml-template.js +4 -4
  50. package/dist/src/cli/mcp-client.js +5 -2
  51. package/dist/src/cli/mcp-tools/agent-tools.js +4 -1
  52. package/dist/src/cli/mcp-tools/hive-mind-tools.js +4 -3
  53. package/dist/src/cli/mcp-tools/hooks-tools.js +6 -5
  54. package/dist/src/cli/mcp-tools/json-store.js +3 -2
  55. package/dist/src/cli/mcp-tools/neural-tools.js +2 -1
  56. package/dist/src/cli/mcp-tools/performance-tools.js +2 -1
  57. package/dist/src/cli/mcp-tools/session-tools.js +4 -2
  58. package/dist/src/cli/mcp-tools/spell-tools.js +2 -1
  59. package/dist/src/cli/memory/bridge-core.js +8 -2
  60. package/dist/src/cli/memory/controllers/_shared.js +8 -2
  61. package/dist/src/cli/memory/controllers/nightly-learner.js +2 -1
  62. package/dist/src/cli/memory/domain/services/memory-domain-service.js +2 -1
  63. package/dist/src/cli/memory/entries-write.js +2 -1
  64. package/dist/src/cli/memory/intelligence.js +2 -1
  65. package/dist/src/cli/memory/persistent-sona.js +1 -3
  66. package/dist/src/cli/memory/types.js +2 -3
  67. package/dist/src/cli/memory/verify.js +3 -2
  68. package/dist/src/cli/movector/graph-analyzer.js +14 -3
  69. package/dist/src/cli/neural/pattern-learner.js +4 -2
  70. package/dist/src/cli/neural/reasoning-bank.js +2 -1
  71. package/dist/src/cli/neural/reasoningbank-adapter.js +2 -1
  72. package/dist/src/cli/neural/sona-manager.js +4 -3
  73. package/dist/src/cli/production/monitoring.js +2 -1
  74. package/dist/src/cli/services/daemon-service.js +57 -46
  75. package/dist/src/cli/services/daemon-spell-executor.js +3 -2
  76. package/dist/src/cli/services/headless-worker-executor.js +4 -2
  77. package/dist/src/cli/services/learning-service.js +2 -1
  78. package/dist/src/cli/services/worker-daemon.js +2 -1
  79. package/dist/src/cli/shared/hooks/example-usage.js +3 -2
  80. package/dist/src/cli/shared/hooks/safety/git-commit.js +11 -2
  81. package/dist/src/cli/shared/hooks/session-hooks.js +3 -2
  82. package/dist/src/cli/shared/hooks/task-hooks.js +2 -1
  83. package/dist/src/cli/shared/mcp/session-manager.js +2 -1
  84. package/dist/src/cli/shared/plugins/official/hive-mind-plugin.js +2 -1
  85. package/dist/src/cli/shared/plugins/official/maestro-plugin.js +3 -2
  86. package/dist/src/cli/shared/security/index.js +6 -2
  87. package/dist/src/cli/shared/utils/atomic-file-write.js +9 -1
  88. package/dist/src/cli/shared/utils/id.js +54 -0
  89. package/dist/src/cli/shared/utils/platform.js +36 -5
  90. package/dist/src/cli/spells/commands/destructive-pattern-checker.js +6 -1
  91. package/dist/src/cli/spells/core/dry-run-validator.js +2 -1
  92. package/dist/src/cli/spells/core/interpolation.js +8 -3
  93. package/dist/src/cli/spells/core/permission-resolver.js +2 -2
  94. package/dist/src/cli/spells/core/runner.js +2 -1
  95. package/dist/src/cli/spells/core/shell.js +7 -8
  96. package/dist/src/cli/spells/factory/runner-bridge.js +3 -2
  97. package/dist/src/cli/spells/factory/runner-factory.js +3 -2
  98. package/dist/src/cli/spells/scheduler/scheduler.js +2 -1
  99. package/dist/src/cli/swarm/consensus/gossip.js +2 -1
  100. package/dist/src/cli/swarm/federation-hub.js +5 -4
  101. package/dist/src/cli/swarm/queen-coordinator.js +5 -4
  102. package/dist/src/cli/swarm/shared/events.js +2 -1
  103. package/dist/src/cli/swarm/unified-coordinator.js +3 -2
  104. package/dist/src/cli/version.js +1 -1
  105. package/package.json +2 -2
  106. package/retired-files.json +0 -72
  107. package/dist/src/cli/mcp-tools/coordination-tools.js +0 -86
  108. package/dist/src/cli/shared/security/secure-random.js +0 -142
@@ -10,6 +10,7 @@
10
10
  *
11
11
  * @module moflo/production/monitoring
12
12
  */
13
+ import { generateId } from '../shared/utils/id.js';
13
14
  // ============================================================================
14
15
  // Default Configuration
15
16
  // ============================================================================
@@ -308,7 +309,7 @@ export class MonitoringHooks {
308
309
  Date.now() - a.timestamp < 60000);
309
310
  if (!recentAlert) {
310
311
  this.alerts.push({
311
- id: `alert_${Date.now()}_${Math.random().toString(36).substring(7)}`,
312
+ id: generateId('alert', { separator: '_' }),
312
313
  level,
313
314
  metric: name,
314
315
  message: `${name} exceeded ${level} threshold: ${value} >= ${threshold}`,
@@ -10,11 +10,15 @@
10
10
  */
11
11
  import * as fs from 'fs';
12
12
  import { createHash } from 'crypto';
13
- import { dirname, join, resolve } from 'path';
13
+ import { basename, dirname, join, resolve } from 'path';
14
14
  import { homedir } from 'os';
15
15
  import { execSync } from 'child_process';
16
16
  import { locateMofloCliBin } from './moflo-require.js';
17
17
  import { errorDetail } from '../shared/utils/error-detail.js';
18
+ /** Real process boundary. Kept thin so the stub and the default stay swappable. */
19
+ const execSyncRunner = (command, options) => {
20
+ execSync(command, options);
21
+ };
18
22
  // ---------------------------------------------------------------------------
19
23
  // Constants
20
24
  // ---------------------------------------------------------------------------
@@ -27,37 +31,39 @@ const SCHTASKS_NAME = 'MoFloDaemon';
27
31
  /**
28
32
  * Check if the daemon service is registered for the current platform.
29
33
  */
30
- export function isDaemonInstalled(projectRoot) {
34
+ export function isDaemonInstalled(projectRoot, options = {}) {
31
35
  const resolvedRoot = resolve(projectRoot);
32
- const platform = process.platform;
36
+ const platform = options.platform ?? process.platform;
37
+ const run = options.runCommand ?? execSyncRunner;
33
38
  if (platform === 'darwin') {
34
- return isDaemonInstalledMacOS(resolvedRoot);
39
+ return isDaemonInstalledMacOS(resolvedRoot, options);
35
40
  }
36
41
  else if (platform === 'linux') {
37
- return isDaemonInstalledLinux(resolvedRoot);
42
+ return isDaemonInstalledLinux(resolvedRoot, options);
38
43
  }
39
44
  else if (platform === 'win32') {
40
- return isDaemonInstalledWindows(resolvedRoot);
45
+ return isDaemonInstalledWindows(resolvedRoot, run);
41
46
  }
42
47
  return false;
43
48
  }
44
49
  /**
45
50
  * Install the daemon as an OS-native login service.
46
51
  */
47
- export function installDaemonService(projectRoot) {
52
+ export function installDaemonService(projectRoot, options = {}) {
48
53
  const resolvedRoot = resolve(projectRoot);
49
54
  validateProjectRoot(resolvedRoot);
50
- const platform = process.platform;
55
+ const platform = options.platform ?? process.platform;
56
+ const run = options.runCommand ?? execSyncRunner;
51
57
  const nodePath = process.execPath;
52
58
  const cliPath = resolveCliPath();
53
59
  if (platform === 'darwin') {
54
- return installMacOS(resolvedRoot, nodePath, cliPath);
60
+ return installMacOS(resolvedRoot, nodePath, cliPath, options);
55
61
  }
56
62
  else if (platform === 'linux') {
57
- return installLinux(resolvedRoot, nodePath, cliPath);
63
+ return installLinux(resolvedRoot, nodePath, cliPath, run, options);
58
64
  }
59
65
  else if (platform === 'win32') {
60
- return installWindows(resolvedRoot, nodePath, cliPath);
66
+ return installWindows(resolvedRoot, nodePath, cliPath, run);
61
67
  }
62
68
  return {
63
69
  success: false,
@@ -68,18 +74,19 @@ export function installDaemonService(projectRoot) {
68
74
  /**
69
75
  * Uninstall the daemon OS-native login service.
70
76
  */
71
- export function uninstallDaemonService(projectRoot) {
77
+ export function uninstallDaemonService(projectRoot, options = {}) {
72
78
  const resolvedRoot = resolve(projectRoot);
73
79
  validateProjectRoot(resolvedRoot);
74
- const platform = process.platform;
80
+ const platform = options.platform ?? process.platform;
81
+ const run = options.runCommand ?? execSyncRunner;
75
82
  if (platform === 'darwin') {
76
- return uninstallMacOS(resolvedRoot);
83
+ return uninstallMacOS(resolvedRoot, run, options);
77
84
  }
78
85
  else if (platform === 'linux') {
79
- return uninstallLinux(resolvedRoot);
86
+ return uninstallLinux(resolvedRoot, run, options);
80
87
  }
81
88
  else if (platform === 'win32') {
82
- return uninstallWindows(resolvedRoot);
89
+ return uninstallWindows(resolvedRoot, run);
83
90
  }
84
91
  return {
85
92
  success: false,
@@ -89,9 +96,9 @@ export function uninstallDaemonService(projectRoot) {
89
96
  // ---------------------------------------------------------------------------
90
97
  // macOS — launchd
91
98
  // ---------------------------------------------------------------------------
92
- function plistPath(projectRoot) {
99
+ function plistPath(projectRoot, options = {}) {
93
100
  const slug = projectRootSlug(projectRoot);
94
- return join(homedir(), 'Library', 'LaunchAgents', `${PLIST_LABEL}.${slug}.plist`);
101
+ return join(options.homeDir ?? homedir(), 'Library', 'LaunchAgents', `${PLIST_LABEL}.${slug}.plist`);
95
102
  }
96
103
  function generatePlist(projectRoot, nodePath, cliPath) {
97
104
  const slug = projectRootSlug(projectRoot);
@@ -128,8 +135,8 @@ function generatePlist(projectRoot, nodePath, cliPath) {
128
135
  '',
129
136
  ].join('\n');
130
137
  }
131
- function installMacOS(projectRoot, nodePath, cliPath) {
132
- const dest = plistPath(projectRoot);
138
+ function installMacOS(projectRoot, nodePath, cliPath, options) {
139
+ const dest = plistPath(projectRoot, options);
133
140
  const dir = dirname(dest);
134
141
  fs.mkdirSync(dir, { recursive: true });
135
142
  const content = generatePlist(projectRoot, nodePath, cliPath);
@@ -140,28 +147,33 @@ function installMacOS(projectRoot, nodePath, cliPath) {
140
147
  message: `Daemon service installed at ${dest}. It will start automatically on login.`,
141
148
  };
142
149
  }
143
- function uninstallMacOS(projectRoot) {
144
- const dest = plistPath(projectRoot);
150
+ function uninstallMacOS(projectRoot, run, options) {
151
+ const dest = plistPath(projectRoot, options);
145
152
  if (!fs.existsSync(dest)) {
146
153
  return { success: true, message: 'Daemon service is not installed.' };
147
154
  }
148
155
  // Unload before removing (ignore errors — may not be loaded)
149
156
  try {
150
- execSync(`launchctl unload "${dest}"`, { timeout: 5000, stdio: 'ignore' });
157
+ run(`launchctl unload "${dest}"`, { timeout: 5000, stdio: 'ignore' });
151
158
  }
152
159
  catch { /* not loaded — fine */ }
153
160
  fs.unlinkSync(dest);
154
161
  return { success: true, message: `Daemon service removed from ${dest}.` };
155
162
  }
156
- function isDaemonInstalledMacOS(projectRoot) {
157
- return fs.existsSync(plistPath(projectRoot));
163
+ function isDaemonInstalledMacOS(projectRoot, options) {
164
+ return fs.existsSync(plistPath(projectRoot, options));
158
165
  }
159
166
  // ---------------------------------------------------------------------------
160
167
  // Linux — systemd --user
161
168
  // ---------------------------------------------------------------------------
162
- function systemdUnitPath(projectRoot) {
169
+ function systemdUnitPath(projectRoot, options = {}) {
163
170
  const slug = projectRootSlug(projectRoot);
164
- const configDir = process.env.XDG_CONFIG_HOME || join(homedir(), '.config');
171
+ // An injected homeDir is a sandbox boundary — XDG_CONFIG_HOME must not
172
+ // override it, or an ambient value would redirect the write back to the
173
+ // developer's real config tree.
174
+ const configDir = options.homeDir
175
+ ? join(options.homeDir, '.config')
176
+ : process.env.XDG_CONFIG_HOME || join(homedir(), '.config');
165
177
  return join(configDir, 'systemd', 'user', `${SYSTEMD_UNIT.replace('.service', '')}-${slug}.service`);
166
178
  }
167
179
  function generateSystemdUnit(projectRoot, nodePath, cliPath) {
@@ -183,17 +195,16 @@ function generateSystemdUnit(projectRoot, nodePath, cliPath) {
183
195
  '',
184
196
  ].join('\n');
185
197
  }
186
- function installLinux(projectRoot, nodePath, cliPath) {
187
- const dest = systemdUnitPath(projectRoot);
198
+ function installLinux(projectRoot, nodePath, cliPath, run, options) {
199
+ const dest = systemdUnitPath(projectRoot, options);
188
200
  const dir = dirname(dest);
189
201
  fs.mkdirSync(dir, { recursive: true });
190
202
  const content = generateSystemdUnit(projectRoot, nodePath, cliPath);
191
203
  fs.writeFileSync(dest, content, 'utf-8');
192
204
  // Reload systemd and enable
193
205
  try {
194
- execSync('systemctl --user daemon-reload', { timeout: 10000, stdio: 'ignore' });
195
- const unitName = dest.split('/').pop();
196
- execSync(`systemctl --user enable ${unitName}`, { timeout: 10000, stdio: 'ignore' });
206
+ run('systemctl --user daemon-reload', { timeout: 10000, stdio: 'ignore' });
207
+ run(`systemctl --user enable ${basename(dest)}`, { timeout: 10000, stdio: 'ignore' });
197
208
  }
198
209
  catch {
199
210
  // systemctl may not be available in all environments
@@ -204,28 +215,28 @@ function installLinux(projectRoot, nodePath, cliPath) {
204
215
  message: `Daemon service installed at ${dest}. It will start automatically on login.`,
205
216
  };
206
217
  }
207
- function uninstallLinux(projectRoot) {
208
- const dest = systemdUnitPath(projectRoot);
218
+ function uninstallLinux(projectRoot, run, options) {
219
+ const dest = systemdUnitPath(projectRoot, options);
209
220
  if (!fs.existsSync(dest)) {
210
221
  return { success: true, message: 'Daemon service is not installed.' };
211
222
  }
212
223
  // Disable and stop before removing
213
224
  try {
214
- const unitName = dest.split('/').pop();
215
- execSync(`systemctl --user disable ${unitName}`, { timeout: 10000, stdio: 'ignore' });
216
- execSync(`systemctl --user stop ${unitName}`, { timeout: 10000, stdio: 'ignore' });
225
+ const unitName = basename(dest);
226
+ run(`systemctl --user disable ${unitName}`, { timeout: 10000, stdio: 'ignore' });
227
+ run(`systemctl --user stop ${unitName}`, { timeout: 10000, stdio: 'ignore' });
217
228
  }
218
229
  catch { /* may not be running */ }
219
230
  fs.unlinkSync(dest);
220
231
  // Reload systemd
221
232
  try {
222
- execSync('systemctl --user daemon-reload', { timeout: 10000, stdio: 'ignore' });
233
+ run('systemctl --user daemon-reload', { timeout: 10000, stdio: 'ignore' });
223
234
  }
224
235
  catch { /* ignore */ }
225
236
  return { success: true, message: `Daemon service removed from ${dest}.` };
226
237
  }
227
- function isDaemonInstalledLinux(projectRoot) {
228
- return fs.existsSync(systemdUnitPath(projectRoot));
238
+ function isDaemonInstalledLinux(projectRoot, options) {
239
+ return fs.existsSync(systemdUnitPath(projectRoot, options));
229
240
  }
230
241
  // ---------------------------------------------------------------------------
231
242
  // Windows — Task Scheduler
@@ -234,12 +245,12 @@ function schtasksName(projectRoot) {
234
245
  const slug = projectRootSlug(projectRoot);
235
246
  return `${SCHTASKS_NAME}-${slug}`;
236
247
  }
237
- function installWindows(projectRoot, nodePath, cliPath) {
248
+ function installWindows(projectRoot, nodePath, cliPath, run) {
238
249
  const taskName = schtasksName(projectRoot);
239
250
  // Build schtasks command — ONLOGON trigger, user-level
240
251
  // Use /F to force overwrite if already exists (idempotent)
241
252
  try {
242
- execSync(`schtasks /Create /TN "${taskName}" /TR "\\"${nodePath}\\" \\"${cliPath}\\" daemon start --foreground --quiet" /SC ONLOGON /F`, { timeout: 15000, windowsHide: true, cwd: projectRoot, stdio: 'ignore' });
253
+ run(`schtasks /Create /TN "${taskName}" /TR "\\"${nodePath}\\" \\"${cliPath}\\" daemon start --foreground --quiet" /SC ONLOGON /F`, { timeout: 15000, windowsHide: true, cwd: projectRoot, stdio: 'ignore' });
243
254
  }
244
255
  catch (err) {
245
256
  return {
@@ -254,10 +265,10 @@ function installWindows(projectRoot, nodePath, cliPath) {
254
265
  message: `Daemon task "${taskName}" registered in Task Scheduler. It will start automatically on login.`,
255
266
  };
256
267
  }
257
- function uninstallWindows(projectRoot) {
268
+ function uninstallWindows(projectRoot, run) {
258
269
  const taskName = schtasksName(projectRoot);
259
270
  try {
260
- execSync(`schtasks /Delete /TN "${taskName}" /F`, { timeout: 15000, windowsHide: true, stdio: 'ignore' });
271
+ run(`schtasks /Delete /TN "${taskName}" /F`, { timeout: 15000, windowsHide: true, stdio: 'ignore' });
261
272
  }
262
273
  catch {
263
274
  // schtasks /Delete /F returns non-zero when task doesn't exist
@@ -265,10 +276,10 @@ function uninstallWindows(projectRoot) {
265
276
  }
266
277
  return { success: true, message: `Daemon task "${taskName}" removed from Task Scheduler.` };
267
278
  }
268
- function isDaemonInstalledWindows(projectRoot) {
279
+ function isDaemonInstalledWindows(projectRoot, run) {
269
280
  const taskName = schtasksName(projectRoot);
270
281
  try {
271
- execSync(`schtasks /Query /TN "${taskName}"`, {
282
+ run(`schtasks /Query /TN "${taskName}"`, {
272
283
  timeout: 10000,
273
284
  windowsHide: true,
274
285
  stdio: 'ignore',
@@ -8,6 +8,7 @@
8
8
  */
9
9
  import { errorDetail } from '../shared/utils/error-detail.js';
10
10
  import { loadSpellEngine, } from './engine-loader.js';
11
+ import { generateId } from '../shared/utils/id.js';
11
12
  export class DaemonSpellExecutor {
12
13
  registry;
13
14
  projectRoot;
@@ -39,11 +40,11 @@ export class DaemonSpellExecutor {
39
40
  this.registry.invalidate();
40
41
  const loaded = this.registry.resolve(spellName);
41
42
  if (!loaded) {
42
- return failedResult(`scheduled-${spellName}-${Date.now()}`, 'STEP_EXECUTION_FAILED', `Spell not found in grimoire: ${spellName}`);
43
+ return failedResult(generateId(`scheduled-${spellName}`), 'STEP_EXECUTION_FAILED', `Spell not found in grimoire: ${spellName}`);
43
44
  }
44
45
  const engine = await this.ensureEngine();
45
46
  const definition = this.applyMofloLevel(loaded.definition, mofloLevel);
46
- const spellId = `scheduled-${loaded.definition.name}-${Date.now()}`;
47
+ const spellId = generateId(`scheduled-${loaded.definition.name}`);
47
48
  const onAbort = () => {
48
49
  try {
49
50
  engine.bridgeCancelSpell(spellId);
@@ -16,6 +16,8 @@ import { EventEmitter } from 'events';
16
16
  import { existsSync, readFileSync, readdirSync, mkdirSync, writeFileSync } from 'fs';
17
17
  import { join } from 'path';
18
18
  import { errorDetail } from '../shared/utils/error-detail.js';
19
+ import { randomSuffix } from '../shared/utils/id.js';
20
+ import { generateId } from '../shared/utils/id.js';
19
21
  // ============================================
20
22
  // Constants
21
23
  // ============================================
@@ -560,7 +562,7 @@ export class HeadlessWorkerExecutor extends EventEmitter {
560
562
  const baseConfig = HEADLESS_WORKER_CONFIGS[workerType];
561
563
  const headless = { ...baseConfig.headless, ...configOverrides };
562
564
  const startTime = Date.now();
563
- const executionId = `${workerType}_${startTime}_${Math.random().toString(36).slice(2, 8)}`;
565
+ const executionId = `${workerType}_${startTime}_${randomSuffix()}`;
564
566
  this.emit('start', { executionId, workerType, config: headless });
565
567
  try {
566
568
  // Build context from file patterns
@@ -1023,7 +1025,7 @@ ${ioInstructions}`;
1023
1025
  sandboxMode: 'strict',
1024
1026
  workerType,
1025
1027
  timestamp: new Date(),
1026
- executionId: `error_${Date.now()}`,
1028
+ executionId: generateId('error', { separator: '_' }),
1027
1029
  error,
1028
1030
  };
1029
1031
  }
@@ -17,6 +17,7 @@ import { dirname } from 'path';
17
17
  import { memoryDbPath } from './moflo-paths.js';
18
18
  import { openDaemonDatabase } from '../memory/daemon-backend.js';
19
19
  import { createNeuralEmbeddingProvider, } from './neural-embedding-provider.js';
20
+ import { generateId } from '../shared/utils/id.js';
20
21
  // ============================================================================
21
22
  // Configuration
22
23
  // ============================================================================
@@ -319,7 +320,7 @@ export class LearningService {
319
320
  this.dirty = true;
320
321
  return { id: existingId, action: 'updated', similarity: ltResults[0].similarity };
321
322
  }
322
- const id = `pat_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
323
+ const id = generateId('pat', { separator: '_' });
323
324
  const embeddingJson = JSON.stringify(Array.from(embedding));
324
325
  dbRun(this.db, `
325
326
  INSERT INTO learned_patterns (id, pattern, domain, quality, uses, tier, embedding, created_at)
@@ -36,6 +36,7 @@ import { CircuitBreaker } from '../production/circuit-breaker.js';
36
36
  import { errorDetail } from '../shared/utils/error-detail.js';
37
37
  import { readLoadAverage } from '../shared/utils/load-average.js';
38
38
  import { readMofloEnv } from './env-compat.js';
39
+ import { generateId } from '../shared/utils/id.js';
39
40
  /**
40
41
  * Runtime allow-list of known {@link WorkerType} values. Used by
41
42
  * `initializeWorkerStates` to silently drop entries from a stale
@@ -787,7 +788,7 @@ export class WorkerDaemon extends EventEmitter {
787
788
  */
788
789
  async executeWorker(workerConfig) {
789
790
  const state = this.workers.get(workerConfig.type);
790
- const workerId = `${workerConfig.type}_${Date.now()}`;
791
+ const workerId = generateId(workerConfig.type, { separator: '_' });
791
792
  const startTime = Date.now();
792
793
  // The controller is parked on shared state so setWorkerEnabled(false) can
793
794
  // interrupt this in-flight run; runWithTimeout also aborts it on timeout
@@ -119,7 +119,7 @@ export function setupPerformanceHooks() {
119
119
  if (!toolName) {
120
120
  return { success: true };
121
121
  }
122
- const metricId = `tool-${toolName}-${Date.now()}`;
122
+ const metricId = generateId(`tool-${toolName}`);
123
123
  performanceMetrics.set(metricId, { start: Date.now() });
124
124
  return {
125
125
  success: true,
@@ -216,7 +216,7 @@ export function setupSessionHooks() {
216
216
  let activeSession = null;
217
217
  // Initialize session
218
218
  registry.register(HookEvent.SessionStart, async (context) => {
219
- const sessionId = context.session?.id || `session-${Date.now()}`;
219
+ const sessionId = context.session?.id || generateId('session');
220
220
  activeSession = {
221
221
  id: sessionId,
222
222
  startTime: new Date(),
@@ -346,6 +346,7 @@ export async function runDemo() {
346
346
  }
347
347
  // Run demo if executed directly
348
348
  import { pathToFileURL as _pfu_eu } from 'url';
349
+ import { generateId } from '../utils/id.js';
349
350
  if (import.meta.url === _pfu_eu(process.argv[1]).href) {
350
351
  runDemo().catch(console.error);
351
352
  }
@@ -8,6 +8,7 @@
8
8
  * @module v3/shared/hooks/safety/git-commit
9
9
  */
10
10
  import { HookEvent, HookPriority, } from '../types.js';
11
+ import { escapeShellArg } from '../../utils/platform.js';
11
12
  const COMMIT_TYPE_PATTERNS = [
12
13
  {
13
14
  keywords: ['add', 'implement', 'create', 'introduce', 'new'],
@@ -137,9 +138,17 @@ export class GitCommitHook {
137
138
  const message = messageMatch[1];
138
139
  const branchName = context.metadata?.branchName;
139
140
  const result = await this.processCommitMessage(message, branchName);
140
- // Modify the command with the new message
141
+ // Modify the command with the new message.
142
+ //
143
+ // `escapeShellArg` rather than a hand-rolled quote escape: this rewrites a
144
+ // command that then runs in the consumer's shell, and the Windows branch of
145
+ // the old escape left a trailing backslash escaping its own closing quote
146
+ // (#1419). The replacement is a FUNCTION so that a `$&` or `$1` occurring in
147
+ // the commit message is inserted literally instead of being expanded by
148
+ // String.replace's substitution syntax.
141
149
  if (result.success && result.modifiedMessage !== message) {
142
- const modifiedCommand = command.replace(/-m\s+["'][^"']+["']/, `-m "${result.modifiedMessage.replace(/"/g, '\\"')}"`);
150
+ const replacement = `-m ${escapeShellArg(result.modifiedMessage ?? '')}`;
151
+ const modifiedCommand = command.replace(/-m\s+["'][^"']+["']/, () => replacement);
143
152
  return {
144
153
  ...result,
145
154
  data: {
@@ -7,6 +7,7 @@
7
7
  * @module v3/shared/hooks/session-hooks
8
8
  */
9
9
  import { HookEvent, HookPriority, } from './types.js';
10
+ import { generateId } from '../utils/id.js';
10
11
  /**
11
12
  * In-memory session storage (for testing and fallback)
12
13
  */
@@ -84,7 +85,7 @@ export class SessionHooksManager {
84
85
  * Handle session start
85
86
  */
86
87
  async handleSessionStart(context) {
87
- this.currentSessionId = context.session?.id || `session-${Date.now()}`;
88
+ this.currentSessionId = context.session?.id || generateId('session');
88
89
  this.sessionStartTime = new Date();
89
90
  this.resetActivity();
90
91
  return {
@@ -189,7 +190,7 @@ export class SessionHooksManager {
189
190
  warnings.push(`${incompleteTasks.length} tasks were incomplete when session ended`);
190
191
  }
191
192
  // Update current session tracking
192
- this.currentSessionId = `session-${Date.now()}-restored`;
193
+ this.currentSessionId = `${generateId('session')}-restored`;
193
194
  this.sessionStartTime = new Date();
194
195
  this.resetActivity();
195
196
  return {
@@ -7,6 +7,7 @@
7
7
  * @module v3/shared/hooks/task-hooks
8
8
  */
9
9
  import { HookEvent, HookPriority, } from './types.js';
10
+ import { generateId } from '../utils/id.js';
10
11
  /**
11
12
  * Task Hooks Manager
12
13
  *
@@ -107,7 +108,7 @@ export class TaskHooksManager {
107
108
  outcome,
108
109
  learningUpdates,
109
110
  patternId: learningUpdates.newPatterns > 0 ? `pattern-${task.id}` : undefined,
110
- trajectoryId: `trajectory-${task.id}-${Date.now()}`,
111
+ trajectoryId: generateId(`trajectory-${task.id}`),
111
112
  };
112
113
  }
113
114
  /**
@@ -13,6 +13,7 @@
13
13
  * - Session cleanup: <10ms
14
14
  */
15
15
  import { EventEmitter } from 'events';
16
+ import { randomBytes } from 'node:crypto';
16
17
  /**
17
18
  * Default session configuration
18
19
  */
@@ -283,7 +284,7 @@ export class SessionManager extends EventEmitter {
283
284
  * Generate a unique session ID
284
285
  */
285
286
  generateSessionId() {
286
- return `session-${++this.sessionCounter}-${Date.now()}-${Math.random().toString(36).substring(2, 8)}`;
287
+ return `session-${++this.sessionCounter}-${Date.now()}-${randomBytes(6).toString('hex')}`;
287
288
  }
288
289
  /**
289
290
  * Clear all sessions
@@ -7,6 +7,7 @@
7
7
  * @module v3/shared/plugins/official/hive-mind
8
8
  */
9
9
  import { HookEvent, HookPriority } from '../../hooks/index.js';
10
+ import { generateId } from '../../utils/id.js';
10
11
  /**
11
12
  * HiveMind Plugin Implementation
12
13
  */
@@ -74,7 +75,7 @@ export class HiveMindPlugin {
74
75
  */
75
76
  async requestDecision(question, options) {
76
77
  const decision = {
77
- id: `decision-${Date.now()}`,
78
+ id: generateId('decision'),
78
79
  question,
79
80
  votes: new Map(),
80
81
  consensusConfidence: 0,
@@ -8,6 +8,7 @@
8
8
  */
9
9
  import { HookEvent, HookPriority } from '../../hooks/index.js';
10
10
  import { errorDetail } from '../../utils/error-detail.js';
11
+ import { generateId } from '../../utils/id.js';
11
12
  /**
12
13
  * Maestro Plugin Implementation
13
14
  */
@@ -73,7 +74,7 @@ export class MaestroPlugin {
73
74
  */
74
75
  createSpell(name, description, steps) {
75
76
  const spell = {
76
- id: `spell-${Date.now()}`,
77
+ id: generateId('spell'),
77
78
  name,
78
79
  description,
79
80
  steps: steps.map((step, index) => ({
@@ -340,7 +341,7 @@ export class MaestroPlugin {
340
341
  }
341
342
  }
342
343
  checkpointSpell(spell) {
343
- spell.checkpoints.set(`checkpoint-${Date.now()}`, {
344
+ spell.checkpoints.set(generateId('checkpoint'), {
344
345
  progress: spell.progress,
345
346
  currentStep: spell.currentStep,
346
347
  stepStatuses: spell.steps.map((s) => ({ id: s.id, status: s.status })),
@@ -5,8 +5,12 @@
5
5
  *
6
6
  * @module v3/shared/security
7
7
  */
8
- // Secure random generation
9
- export { generateSecureId, generateUUID, generateSecureToken, generateShortId, generateSessionId, generateAgentId, generateTaskId, generateMemoryId, generateEventId, generateSwarmId, generatePatternId, generateTrajectoryId, secureRandomInt, secureRandomChoice, secureShuffleArray, } from './secure-random.js';
8
+ // Unique-ID minting lives in `shared/utils/id.ts`, not here. `secure-random.ts`
9
+ // re-exported fifteen ID helpers from this barrel and not one of them ever had
10
+ // an importer, while ~38 call sites went on minting IDs with `Math.random()`.
11
+ // It was deleted in #1423 rather than adopted: its per-domain exports
12
+ // (`generateAgentId`, `generateTaskId`, …) were the sprawl, and one of them
13
+ // shadowed the live `generateMemoryId` in `memory/types.ts`.
10
14
  // Input validation
11
15
  export { validateInput, sanitizeString, validatePath, validateCommand, validateTags, isValidIdentifier, escapeForSql, } from './input-validation.js';
12
16
  //# sourceMappingURL=index.js.map
@@ -42,11 +42,19 @@
42
42
  * @module moflo/cli/shared/utils/atomic-file-write
43
43
  */
44
44
  import * as realFs from 'node:fs';
45
+ // `randomBytes` inline rather than `randomSuffix` from './id.js' (#1423): the
46
+ // session-start launcher dynamically imports this module out of a consumer's
47
+ // `node_modules/moflo/dist/`, so every import edge added here widens a closure
48
+ // that has to resolve before moflo.yaml can be written. `node:crypto` is a
49
+ // builtin and adds no file to that closure; a sibling module would.
50
+ // `tests/bin/launcher-visibility.test.ts` stages that closure by hand and fails
51
+ // the moment it grows.
52
+ import { randomBytes } from 'node:crypto';
45
53
  const IS_WIN32 = process.platform === 'win32';
46
54
  const VERIFY_DEADLINE_MS = 250;
47
55
  const VERIFY_STEP_MS = 10;
48
56
  export function atomicWriteFileSync(targetPath, data, fs = realFs) {
49
- const tmpPath = `${targetPath}.tmp.${process.pid}.${Math.random().toString(36).slice(2, 8)}`;
57
+ const tmpPath = `${targetPath}.tmp.${process.pid}.${randomBytes(4).toString('hex')}`;
50
58
  try {
51
59
  fs.writeFileSync(tmpPath, data);
52
60
  fsyncFile(tmpPath, fs);
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Canonical unique-ID minting.
3
+ *
4
+ * ## Why this module exists
5
+ *
6
+ * Story #801 fixed `agent_spawn` after base-36 slicing of `Math.random()` was
7
+ * *observably* colliding under burst spawns — the comment recording that still
8
+ * sits in `mcp-tools/agent-tools.ts`. The fix was applied there and at a handful
9
+ * of neighbours and stopped; ~38 other sites kept minting IDs the same way, and
10
+ * four separate `generateId` helpers accumulated around them (three live and
11
+ * identically named, in `memory/controllers/_shared.ts`, `memory/bridge-core.ts`
12
+ * and `memory/persistent-sona.ts`, plus an unused `shared/security/secure-random.ts`).
13
+ * This module is the one they all now route through (#1423).
14
+ *
15
+ * A base-36 slice of `Math.random()` carries roughly 31 bits of entropy before
16
+ * slicing and far less after — two entities minted in the same
17
+ * millisecond can share an identity, which for a swarm or agent ID means two
18
+ * distinct actors silently occupying one slot.
19
+ *
20
+ * ## Severity
21
+ *
22
+ * This is a **correctness** fix, not a security one. These IDs are not secrets,
23
+ * capabilities, or session tokens; nothing here depends on unpredictability. The
24
+ * property being restored is uniqueness. (`Math.random()` remains entirely
25
+ * correct for neural weight init, epsilon-greedy exploration, HNSW level
26
+ * selection, and jitter — none of those are touched.)
27
+ */
28
+ import { randomBytes } from 'node:crypto';
29
+ /** Default CSPRNG entropy per ID: 6 bytes = 48 bits = 12 hex characters. */
30
+ const DEFAULT_ENTROPY_BYTES = 6;
31
+ /**
32
+ * Mint a unique ID of the form `<prefix><sep><timestamp><sep><random-hex>`.
33
+ *
34
+ * The timestamp segment is retained from the patterns this replaces: it keeps
35
+ * IDs roughly sortable by creation time and is useful when reading them in logs.
36
+ * Uniqueness rests on the random segment alone.
37
+ */
38
+ export function generateId(prefix, options = {}) {
39
+ const { separator = '-', bytes = DEFAULT_ENTROPY_BYTES, base36Time = false, } = options;
40
+ const timestamp = base36Time ? Date.now().toString(36) : String(Date.now());
41
+ return `${prefix}${separator}${timestamp}${separator}${randomBytes(bytes).toString('hex')}`;
42
+ }
43
+ /**
44
+ * A bare random segment, for the ID shapes that are not
45
+ * `prefix + timestamp + random` — a temp-file suffix, a probe stamp, an ID whose
46
+ * middle segment is a loop index rather than a clock reading.
47
+ *
48
+ * Prefer {@link generateId} when the shape does fit; this exists so the odd ones
49
+ * out do not have to keep reaching for `Math.random()`.
50
+ */
51
+ export function randomSuffix(bytes = DEFAULT_ENTROPY_BYTES) {
52
+ return randomBytes(bytes).toString('hex');
53
+ }
54
+ //# sourceMappingURL=id.js.map