moflo 4.12.4 → 4.12.6

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 (110) 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/doctor-fixes.js +119 -20
  32. package/dist/src/cli/commands/hooks.js +2 -1
  33. package/dist/src/cli/commands/mcp.js +3 -2
  34. package/dist/src/cli/commands/memory.js +2 -1
  35. package/dist/src/cli/commands/performance.js +2 -1
  36. package/dist/src/cli/commands/retire.js +1 -1
  37. package/dist/src/cli/commands/spell-schedule.js +2 -1
  38. package/dist/src/cli/commands/swarm.js +216 -104
  39. package/dist/src/cli/guidance/headless.js +5 -8
  40. package/dist/src/cli/guidance/retriever.js +27 -6
  41. package/dist/src/cli/guidance/ruvbot-integration.js +2 -1
  42. package/dist/src/cli/guidance/uncertainty.js +2 -1
  43. package/dist/src/cli/hooks/bridge/official-hooks-bridge.js +2 -1
  44. package/dist/src/cli/hooks/mcp/index.js +3 -2
  45. package/dist/src/cli/hooks/reasoningbank/index.js +2 -1
  46. package/dist/src/cli/hooks/registry/index.js +2 -1
  47. package/dist/src/cli/hooks/swarm/index.js +6 -5
  48. package/dist/src/cli/init/claudemd-generator.js +2 -1
  49. package/dist/src/cli/init/helpers-generator.js +1 -1
  50. package/dist/src/cli/init/moflo-yaml-template.js +4 -4
  51. package/dist/src/cli/mcp-client.js +5 -2
  52. package/dist/src/cli/mcp-tools/agent-tools.js +4 -1
  53. package/dist/src/cli/mcp-tools/hive-mind-tools.js +4 -3
  54. package/dist/src/cli/mcp-tools/hooks-tools.js +6 -5
  55. package/dist/src/cli/mcp-tools/json-store.js +3 -2
  56. package/dist/src/cli/mcp-tools/neural-tools.js +2 -1
  57. package/dist/src/cli/mcp-tools/performance-tools.js +2 -1
  58. package/dist/src/cli/mcp-tools/session-tools.js +4 -2
  59. package/dist/src/cli/mcp-tools/spell-tools.js +2 -1
  60. package/dist/src/cli/memory/bridge-core.js +8 -2
  61. package/dist/src/cli/memory/controllers/_shared.js +8 -2
  62. package/dist/src/cli/memory/controllers/nightly-learner.js +2 -1
  63. package/dist/src/cli/memory/domain/services/memory-domain-service.js +2 -1
  64. package/dist/src/cli/memory/entries-write.js +2 -1
  65. package/dist/src/cli/memory/intelligence.js +2 -1
  66. package/dist/src/cli/memory/persistent-sona.js +1 -3
  67. package/dist/src/cli/memory/types.js +2 -3
  68. package/dist/src/cli/memory/verify.js +3 -2
  69. package/dist/src/cli/movector/graph-analyzer.js +14 -3
  70. package/dist/src/cli/neural/pattern-learner.js +4 -2
  71. package/dist/src/cli/neural/reasoning-bank.js +2 -1
  72. package/dist/src/cli/neural/reasoningbank-adapter.js +2 -1
  73. package/dist/src/cli/neural/sona-manager.js +4 -3
  74. package/dist/src/cli/production/monitoring.js +2 -1
  75. package/dist/src/cli/services/daemon-service.js +57 -46
  76. package/dist/src/cli/services/daemon-spell-executor.js +3 -2
  77. package/dist/src/cli/services/headless-worker-executor.js +4 -2
  78. package/dist/src/cli/services/learning-service.js +2 -1
  79. package/dist/src/cli/services/project-root.js +17 -1
  80. package/dist/src/cli/services/worker-daemon.js +2 -1
  81. package/dist/src/cli/shared/hooks/example-usage.js +3 -2
  82. package/dist/src/cli/shared/hooks/safety/git-commit.js +11 -2
  83. package/dist/src/cli/shared/hooks/session-hooks.js +3 -2
  84. package/dist/src/cli/shared/hooks/task-hooks.js +2 -1
  85. package/dist/src/cli/shared/mcp/session-manager.js +2 -1
  86. package/dist/src/cli/shared/plugins/official/hive-mind-plugin.js +2 -1
  87. package/dist/src/cli/shared/plugins/official/maestro-plugin.js +3 -2
  88. package/dist/src/cli/shared/security/index.js +6 -2
  89. package/dist/src/cli/shared/utils/atomic-file-write.js +9 -1
  90. package/dist/src/cli/shared/utils/id.js +54 -0
  91. package/dist/src/cli/shared/utils/platform.js +36 -5
  92. package/dist/src/cli/spells/commands/destructive-pattern-checker.js +6 -1
  93. package/dist/src/cli/spells/core/dry-run-validator.js +2 -1
  94. package/dist/src/cli/spells/core/interpolation.js +8 -3
  95. package/dist/src/cli/spells/core/permission-resolver.js +2 -2
  96. package/dist/src/cli/spells/core/runner.js +2 -1
  97. package/dist/src/cli/spells/core/shell.js +7 -8
  98. package/dist/src/cli/spells/factory/runner-bridge.js +3 -2
  99. package/dist/src/cli/spells/factory/runner-factory.js +3 -2
  100. package/dist/src/cli/spells/scheduler/scheduler.js +2 -1
  101. package/dist/src/cli/swarm/consensus/gossip.js +2 -1
  102. package/dist/src/cli/swarm/federation-hub.js +5 -4
  103. package/dist/src/cli/swarm/queen-coordinator.js +5 -4
  104. package/dist/src/cli/swarm/shared/events.js +2 -1
  105. package/dist/src/cli/swarm/unified-coordinator.js +3 -2
  106. package/dist/src/cli/version.js +1 -1
  107. package/package.json +2 -2
  108. package/retired-files.json +0 -72
  109. package/dist/src/cli/mcp-tools/coordination-tools.js +0 -86
  110. package/dist/src/cli/shared/security/secure-random.js +0 -142
@@ -10,8 +10,9 @@
10
10
  import { EventEmitter } from 'node:events';
11
11
  import { reasoningBank } from '../reasoningbank/index.js';
12
12
  import { MessageBus } from '../../swarm/message-bus/message-bus.js';
13
+ import { generateId } from '../../shared/utils/id.js';
13
14
  const DEFAULT_CONFIG = {
14
- agentId: `agent_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`,
15
+ agentId: generateId('agent', { separator: '_' }),
15
16
  agentName: 'anonymous',
16
17
  messageRetention: 3600000, // 1 hour
17
18
  consensusTimeout: 30000, // 30 seconds
@@ -256,7 +257,7 @@ export class SwarmCommunication extends EventEmitter {
256
257
  async broadcastPattern(pattern, targetAgents) {
257
258
  await this.ensureInitialized();
258
259
  const broadcast = {
259
- id: `bc_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
260
+ id: generateId('bc', { separator: '_' }),
260
261
  sourceAgent: this.config.agentId,
261
262
  pattern,
262
263
  broadcastTime: Date.now(),
@@ -348,7 +349,7 @@ export class SwarmCommunication extends EventEmitter {
348
349
  async initiateConsensus(question, options, timeout) {
349
350
  await this.ensureInitialized();
350
351
  const request = {
351
- id: `cons_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
352
+ id: generateId('cons', { separator: '_' }),
352
353
  initiator: this.config.agentId,
353
354
  question,
354
355
  options,
@@ -477,8 +478,8 @@ export class SwarmCommunication extends EventEmitter {
477
478
  async initiateHandoff(toAgent, taskDescription, context) {
478
479
  await this.ensureInitialized();
479
480
  const handoff = {
480
- id: `ho_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
481
- taskId: `task_${Date.now()}`,
481
+ id: generateId('ho', { separator: '_' }),
482
+ taskId: generateId('task', { separator: '_' }),
482
483
  description: taskDescription,
483
484
  fromAgent: this.config.agentId,
484
485
  toAgent,
@@ -50,11 +50,12 @@ After \`npm install\` touches moflo, check \`.moflo/restart-pending.json\` — i
50
50
 
51
51
  ### Monorepos
52
52
 
53
- Moflo state lives at the monorepo root \`.moflo/\` — never run \`flo init\` inside a sub-workspace of an existing moflo project, or the MCP server and CLI silently bind to different daemons (issue #1174).
53
+ Moflo state lives at the monorepo root \`.moflo/\` — never run \`flo init\` inside a sub-workspace of an existing moflo project, or the MCP server and CLI silently bind to different daemons.
54
54
 
55
55
  ### Full Reference
56
56
 
57
57
  - Universal agent rules (memory protocol, git/PR conventions, file org, build/test): \`.claude/guidance/moflo-agent-rules.md\`
58
+ - Fix small defects in the PR you are already in — don't file them: \`.claude/guidance/moflo-inline-fixes.md\`
58
59
  - Subagent spawn protocol: \`.claude/guidance/moflo-subagents.md\`
59
60
  - Task + swarm coordination: \`.claude/guidance/moflo-claude-swarm-cohesion.md\`
60
61
  - CLI, hooks, swarm, memory, moflo.yaml: \`.claude/guidance/moflo-core-guidance.md\`
@@ -983,7 +983,7 @@ switch (command) {
983
983
  if (!/(?:^|&&\\s*|\\|\\|\\s*|;\\s*)\\s*(?:[A-Z_][A-Z0-9_]*=\\S+\\s+)*gh\\s+pr\\s+create\\b/.test(cmd)) break;
984
984
  var s = readState();
985
985
  var missing = [];
986
- if (config.testing_gate && !s.testsRun) missing.push('tests have not run green since the last code edit (run npm test, vitest, jest, pytest, or similar — a run whose output reports failures does not count, #1322)');
986
+ if (config.testing_gate && !s.testsRun) missing.push('tests have not run green since the last code edit (run npm test, vitest, jest, pytest, or similar — a run whose output reports failures does not count)');
987
987
  if (config.simplify_gate && !s.simplifyRun) missing.push('/flo-simplify (or /distill) has not run since the last code edit');
988
988
  if (config.learnings_gate && !s.learningsStored) missing.push('learnings have not been stored (call mcp__moflo__memory_store)');
989
989
  if (missing.length === 0) break;
@@ -214,7 +214,7 @@ gates:
214
214
  memory_first: ${gates}
215
215
  task_create_first: ${gates}
216
216
  context_tracking: ${gates}
217
- verify_before_done: true # Epic #1269/#1294: run /verify before 'gh pr create'. On by default; opt out with false or per-run --no-verify
217
+ verify_before_done: true # Run /verify before 'gh pr create'. On by default; opt out with false or per-run --no-verify
218
218
 
219
219
  # Auto-index on session start
220
220
  auto_index:
@@ -233,7 +233,7 @@ session_continuity:
233
233
  inject: true
234
234
  max_age_hours: 72 # ignore digests older than this when injecting
235
235
 
236
- # Auto-meditate (#1198) — the automatic counterpart to /meditate. When enabled,
236
+ # Auto-meditate — the automatic counterpart to /meditate. When enabled,
237
237
  # moflo recognizes durable lessons in the LIVE session (a tiny answer-first note
238
238
  # on course-corrections / errors / decisions) and distills them into long-term
239
239
  # memory at the next session-start via a cheap headless Haiku pass — deduped.
@@ -241,12 +241,12 @@ session_continuity:
241
241
  auto_meditate:
242
242
  enabled: true
243
243
 
244
- # Spec-Driven Development (Epic #1269) — spec -> plan -> implement -> verify.
244
+ # Spec-Driven Development — spec -> plan -> implement -> verify.
245
245
  # When default is true, every /flo run uses the SDD cycle unless --no-sdd.
246
246
  sdd:
247
247
  default: false
248
248
 
249
- # Auto-merge the PR at the end of a full /flo run once preconditions are met (#1285).
249
+ # Auto-merge the PR at the end of a full /flo run once preconditions are met.
250
250
  # When auto is true, a full /flo run awaits required checks + MERGEABLE state then
251
251
  # merges and deletes the branch, instead of stopping at "PR opened". Opt-in;
252
252
  # override per-run with --merge / --no-merge.
@@ -23,7 +23,11 @@ import { performanceTools } from './mcp-tools/performance-tools.js';
23
23
  // #1353 — `github-tools.ts` is gone. Its four tools made no GitHub API call;
24
24
  // they wrote local JSON and returned `Math.random()` counts, so a `merged: true`
25
25
  // described nothing that happened. Use the `gh` CLI or a GitHub MCP server.
26
- import { coordinationTools } from './mcp-tools/coordination-tools.js';
26
+ // #1428 `coordination-tools.ts` is gone. Its single tool, `coordination_sync`,
27
+ // incremented a counter, slept 50ms under `// Simulate sync`, wrote a local JSON
28
+ // store and returned `synchronized` — it never touched UnifiedSwarmCoordinator.
29
+ // Its agent-side callers were retired by #932; nothing in the tree invoked it.
30
+ // Same disposition as the `github_*` tools in #1353.
27
31
  import { moflodbTools } from './mcp-tools/moflodb-tools.js';
28
32
  // #1349 — handlers for tool names shipped CLI commands were already calling.
29
33
  import { progressTools } from './mcp-tools/progress-tools.js';
@@ -56,7 +60,6 @@ registerTools([
56
60
  ...systemTools,
57
61
  ...neuralTools,
58
62
  ...performanceTools,
59
- ...coordinationTools,
60
63
  ...moflodbTools,
61
64
  ...progressTools,
62
65
  ...coverageTools,
@@ -187,7 +187,10 @@ export const agentTools = [
187
187
  }
188
188
  const task = input.task || config.task || undefined;
189
189
  const routingResult = await determineAgentModel(agentType, config, task);
190
- // Math.random().toString(36) was observably colliding under burst spawns.
190
+ // Base-36 slicing of Math.random() was observably colliding under burst
191
+ // spawns (Story #801). Kept inline rather than routed through
192
+ // shared/utils/id.ts because this ID interpolates the agent type in place
193
+ // of a timestamp; every other site now uses the shared minter (#1423).
191
194
  const agentId = `agent-${agentType}-${randomBytes(12).toString('hex')}`;
192
195
  const capabilities = Array.isArray(config.capabilities)
193
196
  ? config.capabilities.filter((c) => typeof c === 'string')
@@ -14,6 +14,7 @@ import { getSwarmCoordinator } from './swarm-coordinator-singleton.js';
14
14
  import { SUBAGENT_BOOTSTRAP_DIRECTIVE } from '../services/subagent-bootstrap.js';
15
15
  import { validateAgentType } from './agent-tools.js';
16
16
  import { SWARM_CONSTANTS } from '../swarm/types.js';
17
+ import { generateId } from '../shared/utils/id.js';
17
18
  // Namespace constants — avoids hardcoded strings scattered across handlers
18
19
  const HIVE_NS = 'hive-mind';
19
20
  const HIVE_MEMORY_NS = 'hive-mind-memory';
@@ -308,8 +309,8 @@ export const hiveMindTools = [
308
309
  const bus = await getMessageBus();
309
310
  // Initialize write-through adapter (configures hive-mind namespace)
310
311
  await getWriteThroughAdapter();
311
- const hiveId = `hive-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
312
- const queenId = input.queenId || `queen-${Date.now()}`;
312
+ const hiveId = generateId('hive');
313
+ const queenId = input.queenId || generateId('queen');
313
314
  const config = {
314
315
  consensus: resolveConsensus(input.consensus),
315
316
  maxAgents: resolveMaxAgents(input.maxAgents),
@@ -535,7 +536,7 @@ export const hiveMindTools = [
535
536
  const action = input.action;
536
537
  const bus = await getMessageBus();
537
538
  if (action === 'propose') {
538
- const proposalId = `proposal-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
539
+ const proposalId = generateId('proposal');
539
540
  const proposal = {
540
541
  proposalId,
541
542
  type: input.type || 'general',
@@ -8,6 +8,7 @@ import { errorDetail } from '../shared/utils/error-detail.js';
8
8
  import { findProjectRoot } from '../services/project-root.js';
9
9
  import { withSyntheticNotice } from './synthetic.js';
10
10
  import { getMofloVersion } from '../services/moflo-version.js';
11
+ import { generateId } from '../shared/utils/id.js';
11
12
  // Real vector search functions - lazy loaded to avoid circular imports
12
13
  let searchEntriesFn = null;
13
14
  async function getRealSearchFunction() {
@@ -583,7 +584,7 @@ export const hooksPostEdit = {
583
584
  try {
584
585
  const bridge = await import('../memory/memory-bridge.js');
585
586
  feedbackResult = await bridge.bridgeRecordFeedback({
586
- taskId: `edit-${filePath}-${Date.now()}`,
587
+ taskId: generateId(`edit-${filePath}`),
587
588
  success,
588
589
  quality: success ? 0.85 : 0.3,
589
590
  agent,
@@ -1060,7 +1061,7 @@ export const hooksPostTask = {
1060
1061
  learningUpdates: {
1061
1062
  patternsUpdated: feedbackResult?.updated || (success ? 2 : 1),
1062
1063
  newPatterns: success ? 1 : 0,
1063
- trajectoryId: `traj-${Date.now()}`,
1064
+ trajectoryId: generateId('traj'),
1064
1065
  controller: feedbackResult?.controller || 'none',
1065
1066
  outcomePersisted,
1066
1067
  },
@@ -1803,7 +1804,7 @@ export const hooksSessionStart = {
1803
1804
  },
1804
1805
  },
1805
1806
  handler: async (params) => {
1806
- const sessionId = params.sessionId || `session-${Date.now()}`;
1807
+ const sessionId = params.sessionId || generateId('session');
1807
1808
  const restoreLatest = params.restoreLatest;
1808
1809
  const shouldStartDaemon = params.startDaemon !== false;
1809
1810
  // Auto-start daemon if enabled
@@ -1968,7 +1969,7 @@ export const hooksSessionRestore = {
1968
1969
  const restoreAgents = params.restoreAgents !== false;
1969
1970
  const restoreTasks = params.restoreTasks !== false;
1970
1971
  const originalSessionId = requestedId === 'latest' ? `session-${Date.now() - 86400000}` : requestedId;
1971
- const newSessionId = `session-${Date.now()}`;
1972
+ const newSessionId = generateId('session');
1972
1973
  // Get real memory entry count
1973
1974
  const store = loadMemoryStore();
1974
1975
  const memoryEntryCount = Object.keys(store.entries).length;
@@ -2141,7 +2142,7 @@ export const hooksPatternStore = {
2141
2142
  const confidence = params.confidence || 0.8;
2142
2143
  const metadata = params.metadata;
2143
2144
  const timestamp = new Date().toISOString();
2144
- const patternId = `pattern-${Date.now()}-${Math.random().toString(36).substring(7)}`;
2145
+ const patternId = generateId('pattern');
2145
2146
  // Try pattern bridge first (HNSW-indexed raw SQL)
2146
2147
  let bridgeResult = null;
2147
2148
  try {
@@ -1,6 +1,7 @@
1
1
  /**
2
- * Tiny JSON-on-disk store shared by the trimmed MCP-tool surfaces
3
- * (`coordination-tools`, `system-tools`, `performance-tools`).
2
+ * Tiny JSON-on-disk store backing the trimmed MCP-tool surfaces. `performance-tools`
3
+ * is the sole remaining consumer — `coordination-tools` and `system-tools` were the
4
+ * others until their stores were removed.
4
5
  *
5
6
  * Anchors files at `<projectRoot>/.moflo/<subdir>/<file>` via {@link mofloDir},
6
7
  * with the root resolved by {@link findProjectRoot} so an MCP server launched
@@ -16,6 +16,7 @@ import { join } from 'node:path';
16
16
  import { MOFLO_DIR as STORAGE_DIR } from '../services/moflo-paths.js';
17
17
  import { findProjectRoot } from '../services/project-root.js';
18
18
  import { errorDetail } from '../shared/utils/error-detail.js';
19
+ import { generateId } from '../shared/utils/id.js';
19
20
  let realEmbeddings = undefined;
20
21
  let embeddingServiceName = 'none';
21
22
  let embeddingsInitPromise = null;
@@ -212,7 +213,7 @@ const rawNeuralTools = [
212
213
  return { success: true, pattern };
213
214
  }
214
215
  if (action === 'store') {
215
- const patternId = `pattern-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
216
+ const patternId = generateId('pattern');
216
217
  const patternName = input.name || 'Unnamed pattern';
217
218
  // Generate embedding from pattern name/content
218
219
  const embedding = await generateEmbedding(patternName, 384);
@@ -16,6 +16,7 @@
16
16
  import * as os from 'node:os';
17
17
  import { createJsonStore } from './json-store.js';
18
18
  import { readCpuUsagePercent, readLoadAverage, NOT_MEASURED } from '../shared/utils/load-average.js';
19
+ import { generateId } from '../shared/utils/id.js';
19
20
  /**
20
21
  * Cross-platform CPU usage, or null where it cannot be measured (Rule #1).
21
22
  *
@@ -212,7 +213,7 @@ const rawPerformanceTools = [
212
213
  const opsPerSec = Math.round((iterations / durationMs) * 1000);
213
214
  const avgLatencyMs = durationMs / iterations;
214
215
  const memoryDelta = Math.round((memAfter - memBefore) / 1024);
215
- const id = `bench-${suiteName}-${Date.now()}`;
216
+ const id = generateId(`bench-${suiteName}`);
216
217
  const result = {
217
218
  id,
218
219
  name: suiteName,
@@ -4,9 +4,11 @@
4
4
  * Tool definitions for session management with file persistence.
5
5
  */
6
6
  import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, unlinkSync } from 'node:fs';
7
+ import { randomBytes } from 'node:crypto';
7
8
  import { join } from 'node:path';
8
9
  import { MOFLO_DIR as STORAGE_DIR } from '../services/moflo-paths.js';
9
10
  import { findProjectRoot } from '../services/project-root.js';
11
+ import { generateId } from '../shared/utils/id.js';
10
12
  // Storage paths
11
13
  const SESSION_DIR = 'sessions';
12
14
  function storeDir(...parts) {
@@ -112,7 +114,7 @@ export const sessionTools = [
112
114
  required: ['name'],
113
115
  },
114
116
  handler: async (input) => {
115
- const sessionId = `session-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
117
+ const sessionId = `session-${Date.now()}-${randomBytes(6).toString('hex')}`;
116
118
  // Load related data based on options
117
119
  const data = loadRelatedStores({
118
120
  includeMemory: input.includeMemory,
@@ -364,7 +366,7 @@ export const sessionTools = [
364
366
  const importedAt = new Date().toISOString();
365
367
  // A fresh ID keeps an import from silently overwriting a local session
366
368
  // that happens to share the source's ID.
367
- const sessionId = `imported-${Date.now()}`;
369
+ const sessionId = generateId('imported');
368
370
  const agentsImported = incoming.stats?.agents ?? countOf(incoming.data?.agents);
369
371
  const tasksImported = incoming.stats?.tasks ?? countOf(incoming.data?.tasks);
370
372
  const memoryEntriesImported = incoming.stats?.memoryEntries ?? countOf(incoming.data?.memory);
@@ -13,6 +13,7 @@ import { buildGrimoire } from '../services/grimoire-builder.js';
13
13
  import { errorDetail } from '../shared/utils/error-detail.js';
14
14
  import { inferSpellTier } from '../spells/core/spell-tier.js';
15
15
  import { getSharedMemoryAccessor } from '../services/daemon-dashboard.js';
16
+ import { generateId } from '../shared/utils/id.js';
16
17
  // ============================================================================
17
18
  // Constants
18
19
  // ============================================================================
@@ -61,7 +62,7 @@ function trackResult(tracked, result) {
61
62
  // (one cold init per process — see #1020).
62
63
  /** Execute a definition via the engine with tracking and error handling. */
63
64
  async function executeAndTrack(engine, definition, args, options = {}) {
64
- const spellId = `sp-${Date.now()}`;
65
+ const spellId = generateId('sp');
65
66
  const tracked = trackStart(spellId, definition.name, definition.description);
66
67
  try {
67
68
  const projectRoot = findProjectRoot();
@@ -6,7 +6,6 @@
6
6
  */
7
7
  import * as path from 'path';
8
8
  import * as fs from 'fs';
9
- import * as crypto from 'crypto';
10
9
  import { atomicWriteFileSync } from '../services/atomic-file-write.js';
11
10
  import { legacyMemoryDbPath, memoryDbPath, MOFLO_DIR, } from '../services/moflo-paths.js';
12
11
  import { resolveStateRoot } from '../services/project-root.js';
@@ -77,6 +76,7 @@ function getProjectRoot() {
77
76
  }
78
77
  import { ControllerRegistry } from './controller-registry.js';
79
78
  import { errorDetail } from '../shared/utils/error-detail.js';
79
+ import { generateId as mintId } from '../shared/utils/id.js';
80
80
  let registryPromise = null;
81
81
  // Sync handle populated once the promise resolves. Lets sync callers
82
82
  // (refreshVectorStatsCache) read the registry without awaiting.
@@ -233,8 +233,14 @@ export function resolveBridgeDbPath(root, customPath) {
233
233
  function getDbPath(customPath) {
234
234
  return resolveBridgeDbPath(getProjectRoot(), customPath);
235
235
  }
236
+ /**
237
+ * Delegates to the canonical minter, preserving this module's exact ID format
238
+ * (`_` separator, 8 bytes of entropy). The format is pinned rather than
239
+ * normalised because these IDs are persisted to `.moflo/moflo.db` — reshaping
240
+ * them would churn consumer state for no benefit (#1423, Rule #2).
241
+ */
236
242
  export function generateId(prefix) {
237
- return `${prefix}_${Date.now()}_${crypto.randomBytes(8).toString('hex')}`;
243
+ return mintId(prefix, { separator: '_', bytes: 8 });
238
244
  }
239
245
  /**
240
246
  * Returns null if the memory module cannot be loaded or sql.js fails to open —
@@ -4,7 +4,7 @@
4
4
  * Vector-store utilities kept in one place so reflexion / skills /
5
5
  * hierarchical-memory don't each reinvent cosine and BLOB serialization.
6
6
  */
7
- import { randomBytes } from 'node:crypto';
7
+ import { generateId as mintId } from '../../shared/utils/id.js';
8
8
  export function toFloat32(vec) {
9
9
  return vec instanceof Float32Array ? vec : Float32Array.from(vec);
10
10
  }
@@ -118,8 +118,14 @@ function keywordOverlap(lowerQuery, lowerContent) {
118
118
  hits++;
119
119
  return hits / tokens.size;
120
120
  }
121
+ /**
122
+ * Delegates to the canonical minter, preserving this module's exact ID format
123
+ * (`-` separator, base36 timestamp, 6 bytes of entropy). The format is pinned
124
+ * rather than normalised because these IDs are persisted to `.moflo/moflo.db` —
125
+ * reshaping them would churn consumer state for no benefit (#1423, Rule #2).
126
+ */
121
127
  export function generateId(prefix) {
122
- return `${prefix}-${Date.now().toString(36)}-${randomBytes(6).toString('hex')}`;
128
+ return mintId(prefix, { base36Time: true });
123
129
  }
124
130
  /**
125
131
  * Bounded-value helpers shared by controllers. `clamp01` is the common
@@ -15,6 +15,7 @@
15
15
  */
16
16
  import { hasMethod } from './_shared.js';
17
17
  import { errorDetail } from '../../shared/utils/error-detail.js';
18
+ import { generateId } from '../../shared/utils/id.js';
18
19
  export class NightlyLearner {
19
20
  consolidation;
20
21
  reflexion;
@@ -91,7 +92,7 @@ export class NightlyLearner {
91
92
  const self = this;
92
93
  this.timer = setInterval(() => {
93
94
  // Fire-and-forget; consolidate swallows its own errors.
94
- void self.consolidate({ sessionId: `cron-${Date.now()}` });
95
+ void self.consolidate({ sessionId: generateId('cron') });
95
96
  }, intervalMs);
96
97
  if (typeof this.timer.unref === 'function') {
97
98
  this.timer.unref();
@@ -7,6 +7,7 @@
7
7
  * @module v3/memory/domain/services
8
8
  */
9
9
  import { MemoryEntry } from '../entities/memory-entry.js';
10
+ import { generateId } from '../../../shared/utils/id.js';
10
11
  /**
11
12
  * Memory Domain Service
12
13
  *
@@ -246,7 +247,7 @@ export class MemoryDomainService {
246
247
  // Create merged entry
247
248
  return MemoryEntry.create({
248
249
  namespace: primary.namespace,
249
- key: `merged_${Date.now()}`,
250
+ key: generateId('merged', { separator: '_' }),
250
251
  value: {
251
252
  primary: primary.value,
252
253
  related: sorted.slice(1).map((e) => e.value),
@@ -26,6 +26,7 @@ import { serialiseMetadata } from './bridge-entries.js';
26
26
  import { logRoutingFault, writeVectorStatsCache } from './entries-shared.js';
27
27
  import { writeThroughDurable } from '../services/durable-sync.js';
28
28
  import { resolveStateRoot } from '../services/project-root.js';
29
+ import { generateId } from '../shared/utils/id.js';
29
30
  /**
30
31
  * Propagate a just-persisted durable write to the shared store (#1232). The
31
32
  * caller passes the project root the row actually landed under so the flush
@@ -148,7 +149,7 @@ export async function storeEntry(options) {
148
149
  // Ensure schema has all required columns (migration for older DBs)
149
150
  await ensureSchemaColumns(dbPath);
150
151
  const db = openDaemonDatabase(dbPath);
151
- const id = `entry_${Date.now()}_${Math.random().toString(36).substring(7)}`;
152
+ const id = generateId('entry', { separator: '_' });
152
153
  const now = Date.now();
153
154
  // generateEmbedding() throws on embed failure; the outer try/catch returns
154
155
  // success:false rather than inserting a null-embedded row. Opt-out rows
@@ -15,6 +15,7 @@ import { join } from 'node:path';
15
15
  import { errorDetail } from '../shared/utils/error-detail.js';
16
16
  import { findProjectRoot } from '../services/project-root.js';
17
17
  import { MOFLO_DIR, mofloHomeDir } from '../services/moflo-paths.js';
18
+ import { generateId } from '../shared/utils/id.js';
18
19
  // ============================================================================
19
20
  // Persistence Configuration
20
21
  // ============================================================================
@@ -509,7 +510,7 @@ export async function recordStep(step) {
509
510
  // Store in ReasoningBank for retrieval
510
511
  if (reasoningBank) {
511
512
  reasoningBank.store({
512
- id: `step_${Date.now()}_${Math.random().toString(36).substring(7)}`,
513
+ id: generateId('step', { separator: '_' }),
513
514
  type: step.type,
514
515
  embedding,
515
516
  content: step.content,
@@ -12,6 +12,7 @@
12
12
  * @module cli/memory/persistent-sona
13
13
  */
14
14
  import { RvfLearningStore, } from './rvf-learning-store.js';
15
+ import { generateId } from '../shared/utils/id.js';
15
16
  // ===== Constants =====
16
17
  const DEFAULT_PATTERN_THRESHOLD = 0.85;
17
18
  const DEFAULT_MAX_TRAJECTORY_BUFFER = 1000;
@@ -41,9 +42,6 @@ function cosineSimilarity(a, b) {
41
42
  const denom = Math.sqrt(normA) * Math.sqrt(normB);
42
43
  return denom > 0 ? dot / denom : 0;
43
44
  }
44
- function generateId(prefix) {
45
- return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
46
- }
47
45
  // ===== PersistentSonaCoordinator =====
48
46
  /**
49
47
  * Coordinates SONA learning with persistent storage.
@@ -6,13 +6,12 @@
6
6
  *
7
7
  * @module v3/memory/types
8
8
  */
9
+ import { generateId } from '../shared/utils/id.js';
9
10
  /**
10
11
  * Generates a unique memory ID
11
12
  */
12
13
  export function generateMemoryId() {
13
- const timestamp = Date.now().toString(36);
14
- const random = Math.random().toString(36).substring(2, 10);
15
- return `mem_${timestamp}_${random}`;
14
+ return generateId('mem', { separator: '_', base36Time: true });
16
15
  }
17
16
  /**
18
17
  * Creates a default memory entry
@@ -10,6 +10,7 @@
10
10
  */
11
11
  import { openDaemonDatabase } from './daemon-backend.js';
12
12
  import { generateEmbedding } from './embedding-model.js';
13
+ import { generateId } from '../shared/utils/id.js';
13
14
  /**
14
15
  * Verify memory initialization works correctly
15
16
  * Tests: write, read, search, patterns
@@ -33,7 +34,7 @@ export async function verifyMemoryInit(dbPath, options) {
33
34
  });
34
35
  // Test 2: Write entry
35
36
  const writeStart = Date.now();
36
- const testId = `test_${Date.now()}`;
37
+ const testId = generateId('test', { separator: '_' });
37
38
  const testKey = 'verification_test';
38
39
  const testValue = 'This is a verification test entry for memory initialization';
39
40
  try {
@@ -104,7 +105,7 @@ export async function verifyMemoryInit(dbPath, options) {
104
105
  // Test 5: Pattern storage
105
106
  const patternStart = Date.now();
106
107
  try {
107
- const patternId = `pattern_${Date.now()}`;
108
+ const patternId = generateId('pattern', { separator: '_' });
108
109
  db.run(`
109
110
  INSERT INTO patterns (id, name, pattern_type, condition, action, confidence, created_at, updated_at)
110
111
  VALUES (?, 'test-pattern', 'task-routing', 'test condition', 'test action', 0.5, ?, ?)
@@ -62,8 +62,15 @@ async function loadMovector() {
62
62
  */
63
63
  function extractImports(content, _filePath) {
64
64
  const imports = [];
65
- // ES6 import statements
66
- const esImportRegex = /import\s+(?:(?:\{[^}]*\}|\*\s+as\s+\w+|\w+)\s*,?\s*)*\s*from\s*['"]([^'"]+)['"]/g;
65
+ // ES6 import statements. Clause items are separated by a REQUIRED comma
66
+ // rather than the old optional `\s*,?\s*`: with the separator optional, the
67
+ // outer `*` could split a run of word characters many ways, and the regex
68
+ // backtracked exponentially on a pathological source file during indexing
69
+ // (#1418).
70
+ // `(?:type\s+)?` is explicit because the old optional separator absorbed the
71
+ // `type` modifier by accident; without it `import type { T } from 'x'` — the
72
+ // dominant form in this codebase — would stop producing a graph edge.
73
+ const esImportRegex = /import\s+(?:type\s+)?(?:\{[^}]*\}|\*\s+as\s+\w+|\w+)(?:\s*,\s*(?:\{[^}]*\}|\*\s+as\s+\w+|\w+))*\s*from\s*['"]([^'"]+)['"]/g;
67
74
  let match;
68
75
  while ((match = esImportRegex.exec(content)) !== null) {
69
76
  imports.push({ path: match[1], type: 'import' });
@@ -892,5 +899,9 @@ export function exportToDot(result, options = {}) {
892
899
  // ============================================================================
893
900
  // Exports
894
901
  // ============================================================================
895
- export { loadMovector, fallbackMinCut, fallbackLouvain, };
902
+ export { loadMovector, fallbackMinCut, fallbackLouvain,
903
+ // Exported for the #1418 ReDoS regression test: the import regex is only
904
+ // reachable through file I/O otherwise, and a timing assertion needs to call
905
+ // it directly.
906
+ extractImports, };
896
907
  //# sourceMappingURL=graph-analyzer.js.map
@@ -4,6 +4,8 @@
4
4
  * Implements pattern extraction, matching, and evolution for
5
5
  * continuous learning from agent experiences.
6
6
  */
7
+ import { generateId } from '../shared/utils/id.js';
8
+ import { randomSuffix } from '../shared/utils/id.js';
7
9
  /**
8
10
  * Default Pattern Learner configuration
9
11
  */
@@ -106,7 +108,7 @@ export class PatternLearner {
106
108
  }
107
109
  // Create new pattern
108
110
  const pattern = {
109
- patternId: `pat_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`,
111
+ patternId: generateId('pat', { separator: '_' }),
110
112
  name: this.generatePatternName(trajectory),
111
113
  domain: trajectory.domain,
112
114
  embedding,
@@ -247,7 +249,7 @@ export class PatternLearner {
247
249
  newEmbedding[j] = pattern.embedding[j] + noise;
248
250
  }
249
251
  const newPattern = {
250
- patternId: `pat_${Date.now()}_${i}_${Math.random().toString(36).slice(2, 6)}`,
252
+ patternId: `pat_${Date.now()}_${i}_${randomSuffix()}`,
251
253
  name: `${pattern.name}_split_${i}`,
252
254
  domain: pattern.domain,
253
255
  embedding: newEmbedding,
@@ -14,6 +14,7 @@
14
14
  // neural/index → here would leave MofloDbAdapter undefined at module load.
15
15
  import { MofloDbAdapter } from '../memory/moflo-db-adapter.js';
16
16
  import { createDefaultEntry } from '../memory/types.js';
17
+ import { generateId } from '../shared/utils/id.js';
17
18
  /**
18
19
  * Default ReasoningBank configuration
19
20
  */
@@ -284,7 +285,7 @@ export class ReasoningBank {
284
285
  // Compute aggregated embedding
285
286
  const embedding = this.computeAggregateEmbedding(trajectory);
286
287
  const memory = {
287
- memoryId: `mem_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`,
288
+ memoryId: generateId('mem', { separator: '_' }),
288
289
  trajectoryId: trajectory.trajectoryId,
289
290
  strategy,
290
291
  keyLearnings,
@@ -11,6 +11,7 @@
11
11
  */
12
12
  import { createSONAManager } from './sona-manager.js';
13
13
  import { createPatternLearner } from './pattern-learner.js';
14
+ import { generateId } from '../shared/utils/id.js';
14
15
  // ============================================================================
15
16
  // ReasoningBank Adapter Implementation
16
17
  // ============================================================================
@@ -149,7 +150,7 @@ export class ReasoningBankAdapter {
149
150
  const patterns = this.extractPatternsFromTrajectory(trajectory, verdict);
150
151
  for (let i = 0; i < Math.min(patterns.length, maxItems); i++) {
151
152
  const pattern = patterns[i];
152
- const id = `mem_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
153
+ const id = generateId('mem', { separator: '_' });
153
154
  const rbPattern = {
154
155
  id,
155
156
  type: 'reasoning_memory',
@@ -15,6 +15,7 @@ import { BalancedMode } from './modes/balanced.js';
15
15
  import { ResearchMode } from './modes/research.js';
16
16
  import { EdgeMode } from './modes/edge.js';
17
17
  import { BatchMode } from './modes/batch.js';
18
+ import { generateId } from '../shared/utils/id.js';
18
19
  /**
19
20
  * Default mode configurations
20
21
  */
@@ -210,7 +211,7 @@ export class SONAManager {
210
211
  */
211
212
  beginTrajectory(context, domain = 'general') {
212
213
  const startTime = performance.now();
213
- const trajectoryId = `traj_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
214
+ const trajectoryId = generateId('traj', { separator: '_' });
214
215
  const trajectory = {
215
216
  trajectoryId,
216
217
  context,
@@ -313,7 +314,7 @@ export class SONAManager {
313
314
  const startTime = performance.now();
314
315
  const fullPattern = {
315
316
  ...pattern,
316
- patternId: `pat_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`,
317
+ patternId: generateId('pat', { separator: '_' }),
317
318
  createdAt: Date.now(),
318
319
  updatedAt: Date.now(),
319
320
  };
@@ -399,7 +400,7 @@ export class SONAManager {
399
400
  initializeLoRAWeights(domain = 'default') {
400
401
  const config = this.getLoRAConfig();
401
402
  const weights = {
402
- adapterId: `lora_${domain}_${Date.now()}`,
403
+ adapterId: generateId(`lora_${domain}`, { separator: '_' }),
403
404
  A: new Map(),
404
405
  B: new Map(),
405
406
  createdAt: Date.now(),