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
@@ -5,14 +5,14 @@
5
5
  * shell-out where possible). Falls back to running the check's `fix` string
6
6
  * if it looks like an `npx`/`npm`/`claude` command.
7
7
  */
8
- import { appendFileSync, existsSync, mkdirSync, readFileSync, renameSync, rmdirSync, unlinkSync, writeFileSync, readdirSync } from 'fs';
9
- import { join } from 'path';
8
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, realpathSync, renameSync, rmdirSync, unlinkSync, writeFileSync, readdirSync } from 'fs';
9
+ import { basename, dirname, isAbsolute, join, parse, relative, resolve } from 'path';
10
10
  import { output } from '../output.js';
11
11
  import { errorDetail } from '../shared/utils/error-detail.js';
12
12
  import { atomicWriteFileSync } from '../shared/utils/atomic-file-write.js';
13
13
  import { repairHookWiring } from '../services/hook-wiring.js';
14
14
  import { findProjectDaemonPids, getDaemonLockHolder } from '../services/daemon-lock.js';
15
- import { findProjectRoot, resolveStateRoot } from '../services/project-root.js';
15
+ import { findProjectRoot, hasMofloStateMarker, resolveStateRoot } from '../services/project-root.js';
16
16
  import { legacyMemoryDbPath, legacyMemoryDbBakPath, memoryDbPath, mofloDir } from '../services/moflo-paths.js';
17
17
  import { findZombieProcesses } from './doctor-zombies.js';
18
18
  import { loadToolArrays, getTool } from './doctor-checks-functional-shared.js';
@@ -29,6 +29,86 @@ async function runFixCommand(cmd) {
29
29
  return false;
30
30
  }
31
31
  }
32
+ /** `realpathSync` if the path exists, else the absolutized string. */
33
+ function canonical(p) {
34
+ const abs = resolve(p);
35
+ try {
36
+ return realpathSync(abs);
37
+ }
38
+ catch {
39
+ return abs;
40
+ }
41
+ }
42
+ /**
43
+ * Nearest moflo root at or above `from` (inclusive), or null.
44
+ *
45
+ * Shares `hasMofloStateMarker` with `findProjectRoot`'s Pass A rather than
46
+ * restating the marker set: a guard that tests a narrower signal than the
47
+ * resolver it guards is blind exactly where it matters, which is #1431's
48
+ * fourth defect. `findAncestorMofloRoot` is deliberately not reused — it tests
49
+ * only `.moflo/moflo.db`, and it is exclusive of its starting directory.
50
+ */
51
+ function nearestMofloRoot(from) {
52
+ const start = resolve(from);
53
+ const fsRoot = parse(start).root;
54
+ let dir = start;
55
+ while (dir !== fsRoot) {
56
+ // Same skip as Pass A — a marker inside `node_modules` is a vendored
57
+ // package's state, not a project root.
58
+ if (basename(dir) === 'node_modules') {
59
+ dir = dirname(dir);
60
+ continue;
61
+ }
62
+ if (hasMofloStateMarker(dir))
63
+ return dir;
64
+ const parent = dirname(dir);
65
+ if (parent === dir)
66
+ break;
67
+ dir = parent;
68
+ }
69
+ return null;
70
+ }
71
+ /**
72
+ * Resolve the project root the daemon auto-fixes may act on, or null to refuse.
73
+ *
74
+ * The fixes SIGTERM processes and unlink lock files, so an over-broad root is
75
+ * not a cosmetic error. `findProjectRoot()` Pass A deliberately returns the
76
+ * TOPMOST ancestor carrying `.moflo/moflo.db` (#1174, so a monorepo's root
77
+ * daemon is canonical). For a checkout nested inside an unrelated project that
78
+ * walk climbs past our own root and lands on the parent's — whose daemons are
79
+ * not ours to kill. These handlers were previously rooted at `process.cwd()`,
80
+ * where that case degraded to a harmless no-op (`pids` came back empty), so
81
+ * aligning them with `checkDaemonOrphan` must not convert a no-op into a kill.
82
+ *
83
+ * Refuse whenever a NEARER moflo root sits at or above the cwd and differs
84
+ * from the resolved root: that is the nested-checkout signature. Both sides are
85
+ * realpath'd before comparison (Rule #1 §2) — `findProjectRoot` may hand back a
86
+ * raw `CLAUDE_PROJECT_DIR` while the walk yields a resolved path, and on macOS
87
+ * `/var/folders` vs `/private/var/folders` would otherwise compare unequal.
88
+ */
89
+ function daemonFixRoot() {
90
+ const root = findProjectRoot();
91
+ const rootC = canonical(root);
92
+ const cwdC = canonical(process.cwd());
93
+ // Containment first: the cwd must live inside the root we are about to reap
94
+ // in. `findProjectRoot` returns `CLAUDE_PROJECT_DIR` verbatim when set, and
95
+ // that variable is inherited by hooks, test runners, and spawned tools whose
96
+ // cwd may be somewhere else entirely — a temp fixture, `/`, another repo.
97
+ // Without this check the handler happily reaps the daemons of whatever
98
+ // project the environment names, from a cwd with no relationship to it.
99
+ // `relative` + `isAbsolute` rather than string prefixing: on Windows two
100
+ // different drive letters yield an absolute result, which must count as
101
+ // "outside" (Rule #1).
102
+ const rel = relative(rootC, cwdC);
103
+ if (rel !== '' && (rel.startsWith('..') || isAbsolute(rel)))
104
+ return null;
105
+ const nearest = nearestMofloRoot(cwdC);
106
+ // No moflo state anywhere at or above cwd: nothing to disambiguate, and the
107
+ // scan will find no same-project daemons anyway.
108
+ if (nearest === null)
109
+ return root;
110
+ return canonical(nearest) === rootC ? root : null;
111
+ }
32
112
  /**
33
113
  * Fix the `Config File` check by creating the project's JSON config.
34
114
  *
@@ -601,9 +681,13 @@ export async function autoFixCheck(check) {
601
681
  // Also reaps any same-project orphans whose PIDs aren't recorded in the
602
682
  // lock — those are the daemons that survived prior buggy fixes.
603
683
  'Daemon Status': async () => {
604
- const cwd = process.cwd();
684
+ // #1431 same subdirectory-mismatch class as `Daemon Orphan`; this one
685
+ // also reaps and unlinks, so it takes the same guarded root.
686
+ const root = daemonFixRoot();
687
+ if (root === null)
688
+ return false;
605
689
  const { getDaemonLockPayload, reapSameProjectOrphans } = await import('../services/daemon-lock.js');
606
- const payload = getDaemonLockPayload(cwd);
690
+ const payload = getDaemonLockPayload(root);
607
691
  if (payload?.pid && payload.pid > 0) {
608
692
  try {
609
693
  process.kill(payload.pid, 'SIGTERM');
@@ -611,9 +695,9 @@ export async function autoFixCheck(check) {
611
695
  catch { /* already dead */ }
612
696
  }
613
697
  // Wipe other same-project daemons that the lock doesn't account for.
614
- reapSameProjectOrphans(cwd);
615
- const lockFile = join(cwd, '.moflo', 'daemon.lock');
616
- const pidFile = join(cwd, '.moflo', 'daemon.pid');
698
+ reapSameProjectOrphans(root);
699
+ const lockFile = join(root, '.moflo', 'daemon.lock');
700
+ const pidFile = join(root, '.moflo', 'daemon.pid');
617
701
  try {
618
702
  if (existsSync(lockFile))
619
703
  unlinkSync(lockFile);
@@ -629,16 +713,19 @@ export async function autoFixCheck(check) {
629
713
  // bin/session-start-launcher.mjs so the auto-fix matches the launcher's
630
714
  // behavior exactly.
631
715
  'Daemon Version Skew': async () => {
632
- const cwd = process.cwd();
716
+ // #1431 same subdirectory-mismatch class as `Daemon Orphan`.
717
+ const root = daemonFixRoot();
718
+ if (root === null)
719
+ return false;
633
720
  const { getDaemonLockPayload } = await import('../services/daemon-lock.js');
634
- const payload = getDaemonLockPayload(cwd);
721
+ const payload = getDaemonLockPayload(root);
635
722
  if (payload?.pid && payload.pid > 0) {
636
723
  try {
637
724
  process.kill(payload.pid, 'SIGTERM');
638
725
  }
639
726
  catch { /* already dead */ }
640
727
  }
641
- const lockFile = join(cwd, '.moflo', 'daemon.lock');
728
+ const lockFile = join(root, '.moflo', 'daemon.lock');
642
729
  try {
643
730
  if (existsSync(lockFile))
644
731
  unlinkSync(lockFile);
@@ -653,20 +740,27 @@ export async function autoFixCheck(check) {
653
740
  // threaded into `reapSameProjectOrphans` so we don't re-run the
654
741
  // OS process scan inside it.
655
742
  'Daemon Orphan': async () => {
656
- const cwd = process.cwd();
743
+ // #1431 resolve the root the way `checkDaemonOrphan` does, not with
744
+ // `process.cwd()`. The check walks up to the project root; the fix used
745
+ // the raw cwd, so `flo doctor --fix` from a subdirectory scanned a root
746
+ // with no daemons, fell through `pids.length <= 1`, and reported
747
+ // "Fixed: Daemon Orphan" without reaping anything.
748
+ const root = daemonFixRoot();
749
+ if (root === null)
750
+ return false;
657
751
  const { findProjectDaemonPids, getDaemonLockHolder, reapSameProjectOrphans } = await import('../services/daemon-lock.js');
658
- const pids = findProjectDaemonPids(cwd);
752
+ const pids = findProjectDaemonPids(root);
659
753
  if (pids.length <= 1)
660
754
  return true; // already healthy
661
- const lockHolder = getDaemonLockHolder(cwd);
755
+ const lockHolder = getDaemonLockHolder(root);
662
756
  if (lockHolder != null && pids.includes(lockHolder)) {
663
- const { survived } = reapSameProjectOrphans(cwd, process.pid, lockHolder, pids);
757
+ const { survived } = reapSameProjectOrphans(root, process.pid, lockHolder, pids);
664
758
  return survived.length === 0;
665
759
  }
666
760
  // No identifiable canonical daemon — kill them all, clear the lock,
667
761
  // respawn fresh.
668
- const { survived } = reapSameProjectOrphans(cwd, process.pid, undefined, pids);
669
- const lockFile = join(cwd, '.moflo', 'daemon.lock');
762
+ const { survived } = reapSameProjectOrphans(root, process.pid, undefined, pids);
763
+ const lockFile = join(root, '.moflo', 'daemon.lock');
670
764
  try {
671
765
  if (existsSync(lockFile))
672
766
  unlinkSync(lockFile);
@@ -682,16 +776,21 @@ export async function autoFixCheck(check) {
682
776
  // daemon binds the per-project deterministic port and stamps it into
683
777
  // the lock — clients can discover it without guessing.
684
778
  'Daemon Identity Match': async () => {
685
- const cwd = process.cwd();
779
+ // #1431 same resolver as `checkDaemonIdentityMatch`; see the note on
780
+ // the `Daemon Orphan` handler above. Reading the lock from a raw cwd
781
+ // would SIGTERM nothing and unlink a path the daemon never wrote.
782
+ const root = daemonFixRoot();
783
+ if (root === null)
784
+ return false;
686
785
  const { getDaemonLockPayload } = await import('../services/daemon-lock.js');
687
- const payload = getDaemonLockPayload(cwd);
786
+ const payload = getDaemonLockPayload(root);
688
787
  if (payload?.pid && payload.pid > 0) {
689
788
  try {
690
789
  process.kill(payload.pid, 'SIGTERM');
691
790
  }
692
791
  catch { /* already dead */ }
693
792
  }
694
- const lockFile = join(cwd, '.moflo', 'daemon.lock');
793
+ const lockFile = join(root, '.moflo', 'daemon.lock');
695
794
  try {
696
795
  if (existsSync(lockFile))
697
796
  unlinkSync(lockFile);
@@ -11,6 +11,7 @@ import { errorDetail } from '../shared/utils/error-detail.js';
11
11
  // #1315 — the intelligence/maturity probes below read `.moflo/` state; anchor
12
12
  // them at the resolved root so a subdirectory invocation doesn't report zeroes.
13
13
  import { resolveStateRoot } from '../services/project-root.js';
14
+ import { generateId } from '../shared/utils/id.js';
14
15
  // Hook types
15
16
  const HOOK_TYPES = [
16
17
  { value: 'pre-edit', label: 'Pre-Edit', hint: 'Get context before editing files' },
@@ -1204,7 +1205,7 @@ const postTaskCommand = {
1204
1205
  ],
1205
1206
  action: async (ctx) => {
1206
1207
  // Auto-generate task ID if not provided
1207
- const taskId = ctx.flags.taskId || `task_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
1208
+ const taskId = ctx.flags.taskId || generateId('task', { separator: '_' });
1208
1209
  // Default success to true for backward compatibility
1209
1210
  const success = ctx.flags.success !== undefined ? ctx.flags.success : true;
1210
1211
  output.printInfo(`Recording outcome for task: ${output.highlight(taskId)}`);
@@ -11,6 +11,7 @@ import { getServerManager, getMCPServerStatus, } from '../mcp-server.js';
11
11
  import { listMCPTools, callMCPTool, hasTool } from '../mcp-client.js';
12
12
  import { acquireDaemonLock, releaseDaemonLock, getDaemonLockHolder } from '../services/daemon-lock.js';
13
13
  import { readMofloEnv } from '../services/env-compat.js';
14
+ import { generateId } from '../shared/utils/id.js';
14
15
  // MCP tool categories offered by `flo mcp tools --category`.
15
16
  //
16
17
  // #1353 removed the `github` entry along with the four tools that were the
@@ -494,8 +495,8 @@ const execCommand = {
494
495
  }
495
496
  const startTime = performance.now();
496
497
  const result = await callMCPTool(tool, params, {
497
- sessionId: `cli-${Date.now().toString(36)}`,
498
- requestId: `exec-${Date.now()}`,
498
+ sessionId: generateId('cli', { base36Time: true }),
499
+ requestId: generateId('exec'),
499
500
  });
500
501
  const duration = performance.now() - startTime;
501
502
  output.writeln();
@@ -13,6 +13,7 @@ import { errorDetail } from '../shared/utils/error-detail.js';
13
13
  import { memoryDbPath } from '../services/moflo-paths.js';
14
14
  import { resolveBridgeDbPath } from '../memory/bridge-core.js';
15
15
  import { findProjectRoot } from '../services/project-root.js';
16
+ import { generateId } from '../shared/utils/id.js';
16
17
  // Memory backends
17
18
  const BACKENDS = [
18
19
  { value: 'agentdb', label: 'AgentDB', hint: 'Vector database with HNSW approximate-nearest-neighbor (ANN) indexing' },
@@ -1327,7 +1328,7 @@ function saveAndCloseDb(db, _dbPath) {
1327
1328
  db.close();
1328
1329
  }
1329
1330
  function batchGenerateId() {
1330
- return `mem_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
1331
+ return generateId('mem', { separator: '_' });
1331
1332
  }
1332
1333
  function batchStoreEntry(db, key, namespace, content, metadata = {}, tags = [], embedding, embeddingModel, embeddingDimensions) {
1333
1334
  const now = Date.now();
@@ -6,6 +6,7 @@
6
6
  */
7
7
  import { output } from '../output.js';
8
8
  import { readLoadAverage, NOT_MEASURED } from '../shared/utils/load-average.js';
9
+ import { generateId } from '../shared/utils/id.js';
9
10
  // Benchmark subcommand - REAL measurements
10
11
  const benchmarkCommand = {
11
12
  name: 'benchmark',
@@ -165,7 +166,7 @@ const benchmarkCommand = {
165
166
  for (let i = 0; i < Math.min(iterations, 20); i++) {
166
167
  const start = performance.now();
167
168
  await storeEntry({
168
- key: `bench_${Date.now()}_${i}`,
169
+ key: generateId('bench', { separator: '_' }),
169
170
  value: `Benchmark test entry ${i} with some content for testing storage performance`,
170
171
  namespace: 'benchmark',
171
172
  generateEmbeddingFlag: true,
@@ -57,7 +57,7 @@ export const retireCommand = {
57
57
  },
58
58
  {
59
59
  name: 'rebuild-hashes',
60
- description: 'Recompute knownContentHashes[] for every existing entry from full git history (#1133 backfill)',
60
+ description: 'Recompute knownContentHashes[] for every entry from full git history, and drop entries whose path is back in the tree (#1133 backfill, #1414 heal)',
61
61
  type: 'boolean',
62
62
  default: false,
63
63
  },
@@ -19,6 +19,7 @@ import { checkScheduleAcceptance } from '../services/schedule-acceptance-check.j
19
19
  import { reconcileDaemonAutostart } from '../services/daemon-autostart-lifecycle.js';
20
20
  import { isDaemonInstalled } from '../services/daemon-service.js';
21
21
  import { validateSchedule, computeNextRun } from '../spells/scheduler/cron-parser.js';
22
+ import { randomSuffix } from '../shared/utils/id.js';
22
23
  const NAMESPACE_SCHEDULES = 'scheduled-spells';
23
24
  const NAMESPACE_EXECUTIONS = 'schedule-executions';
24
25
  const DEFAULT_EXECUTIONS_LIMIT = 10;
@@ -135,7 +136,7 @@ const createCommand = {
135
136
  output.printWarning(acceptance.message);
136
137
  }
137
138
  // Always create the schedule, regardless of daemon state
138
- const id = `sched-adhoc-${now}-${Math.random().toString(36).slice(2, 8)}`;
139
+ const id = `sched-adhoc-${now}-${randomSuffix()}`;
139
140
  const record = {
140
141
  id,
141
142
  spellName: name,