monomind 2.0.3 → 2.1.3

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 (113) hide show
  1. package/README.md +74 -92
  2. package/package.json +11 -7
  3. package/packages/@monomind/cli/.claude/helpers/control-start.cjs +13 -20
  4. package/packages/@monomind/cli/.claude/helpers/event-logger.cjs +109 -6
  5. package/packages/@monomind/cli/.claude/helpers/graphify-freshen.cjs +19 -17
  6. package/packages/@monomind/cli/.claude/helpers/handlers/agent-start-handler.cjs +60 -28
  7. package/packages/@monomind/cli/.claude/helpers/handlers/capture-handler.cjs +99 -28
  8. package/packages/@monomind/cli/.claude/helpers/handlers/edit-handler.cjs +27 -9
  9. package/packages/@monomind/cli/.claude/helpers/handlers/gates-handler.cjs +18 -9
  10. package/packages/@monomind/cli/.claude/helpers/handlers/loops-status-handler.cjs +1 -1
  11. package/packages/@monomind/cli/.claude/helpers/handlers/route-handler.cjs +33 -4
  12. package/packages/@monomind/cli/.claude/helpers/handlers/session-handler.cjs +75 -45
  13. package/packages/@monomind/cli/.claude/helpers/handlers/session-restore-handler.cjs +0 -12
  14. package/packages/@monomind/cli/.claude/helpers/handlers/task-handler.cjs +53 -22
  15. package/packages/@monomind/cli/.claude/helpers/hook-handler.cjs +75 -4
  16. package/packages/@monomind/cli/.claude/helpers/intelligence.cjs +94 -15
  17. package/packages/@monomind/cli/.claude/helpers/session.cjs +6 -1
  18. package/packages/@monomind/cli/.claude/helpers/statusline.cjs +10 -10
  19. package/packages/@monomind/cli/.claude/helpers/token-tracker.cjs +3 -3
  20. package/packages/@monomind/cli/.claude/helpers/utils/agent-registrations.cjs +41 -0
  21. package/packages/@monomind/cli/.claude/helpers/utils/fs-helpers.cjs +183 -0
  22. package/packages/@monomind/cli/.claude/helpers/utils/micro-agents.cjs +10 -6
  23. package/packages/@monomind/cli/.claude/helpers/utils/monograph.cjs +73 -2
  24. package/packages/@monomind/cli/.claude/helpers/utils/telemetry.cjs +35 -19
  25. package/packages/@monomind/cli/README.md +74 -92
  26. package/packages/@monomind/cli/bin/cli.js +10 -1
  27. package/packages/@monomind/cli/dist/src/browser/workflow/store.d.ts +2 -2
  28. package/packages/@monomind/cli/dist/src/commands/agent-ops.js +9 -15
  29. package/packages/@monomind/cli/dist/src/commands/browse.d.ts +1 -1
  30. package/packages/@monomind/cli/dist/src/commands/cleanup.js +20 -4
  31. package/packages/@monomind/cli/dist/src/commands/completions.js +66 -261
  32. package/packages/@monomind/cli/dist/src/commands/doctor-env-checks.js +5 -0
  33. package/packages/@monomind/cli/dist/src/commands/doctor.js +7 -2
  34. package/packages/@monomind/cli/dist/src/commands/hooks-workers.js +7 -0
  35. package/packages/@monomind/cli/dist/src/commands/index.js +3 -0
  36. package/packages/@monomind/cli/dist/src/commands/mcp.js +11 -6
  37. package/packages/@monomind/cli/dist/src/commands/memory-admin.js +12 -6
  38. package/packages/@monomind/cli/dist/src/commands/memory-transfer.js +6 -13
  39. package/packages/@monomind/cli/dist/src/commands/monograph.js +3 -3
  40. package/packages/@monomind/cli/dist/src/commands/org.d.ts +3 -1
  41. package/packages/@monomind/cli/dist/src/commands/org.js +321 -127
  42. package/packages/@monomind/cli/dist/src/commands/platforms.d.ts +1 -1
  43. package/packages/@monomind/cli/dist/src/commands/start.js +142 -37
  44. package/packages/@monomind/cli/dist/src/commands/tokens.js +21 -3
  45. package/packages/@monomind/cli/dist/src/config-adapter.js +37 -17
  46. package/packages/@monomind/cli/dist/src/index.js +64 -28
  47. package/packages/@monomind/cli/dist/src/init/claudemd-generator.js +44 -11
  48. package/packages/@monomind/cli/dist/src/init/executor.js +93 -55
  49. package/packages/@monomind/cli/dist/src/mcp-client.d.ts +8 -3
  50. package/packages/@monomind/cli/dist/src/mcp-client.js +37 -3
  51. package/packages/@monomind/cli/dist/src/mcp-server.js +8 -2
  52. package/packages/@monomind/cli/dist/src/mcp-tools/agent-tools.d.ts +20 -0
  53. package/packages/@monomind/cli/dist/src/mcp-tools/agent-tools.js +10 -5
  54. package/packages/@monomind/cli/dist/src/mcp-tools/auto-install.d.ts +8 -8
  55. package/packages/@monomind/cli/dist/src/mcp-tools/coherence/types.d.ts +31 -31
  56. package/packages/@monomind/cli/dist/src/mcp-tools/github-tools.js +30 -8
  57. package/packages/@monomind/cli/dist/src/mcp-tools/graphify-tools.js +11 -2
  58. package/packages/@monomind/cli/dist/src/mcp-tools/hive-mind-tools.js +56 -35
  59. package/packages/@monomind/cli/dist/src/mcp-tools/knowledge-tools.js +9 -1
  60. package/packages/@monomind/cli/dist/src/mcp-tools/monograph-compat.js +56 -14
  61. package/packages/@monomind/cli/dist/src/mcp-tools/monograph-tools.js +88 -41
  62. package/packages/@monomind/cli/dist/src/mcp-tools/quality/coverage-analysis/prioritize-gaps.d.ts +36 -36
  63. package/packages/@monomind/cli/dist/src/mcp-tools/quality/security-compliance/detect-secrets.d.ts +4 -4
  64. package/packages/@monomind/cli/dist/src/mcp-tools/swarm-tools.js +6 -4
  65. package/packages/@monomind/cli/dist/src/mcp-tools/system-tools.js +4 -4
  66. package/packages/@monomind/cli/dist/src/mcp-tools/types.d.ts +20 -0
  67. package/packages/@monomind/cli/dist/src/mcp-tools/types.js +36 -1
  68. package/packages/@monomind/cli/dist/src/memory/ewc-consolidation.d.ts +12 -0
  69. package/packages/@monomind/cli/dist/src/memory/hnsw-operations.d.ts +13 -1
  70. package/packages/@monomind/cli/dist/src/memory/hnsw-operations.js +66 -14
  71. package/packages/@monomind/cli/dist/src/memory/intelligence.js +99 -3
  72. package/packages/@monomind/cli/dist/src/memory/memory-bridge.d.ts +7 -0
  73. package/packages/@monomind/cli/dist/src/memory/memory-bridge.js +20 -0
  74. package/packages/@monomind/cli/dist/src/memory/sona-optimizer.d.ts +12 -0
  75. package/packages/@monomind/cli/dist/src/orgrt/broker.d.ts +26 -0
  76. package/packages/@monomind/cli/dist/src/orgrt/broker.js +73 -0
  77. package/packages/@monomind/cli/dist/src/orgrt/bus.d.ts +5 -1
  78. package/packages/@monomind/cli/dist/src/orgrt/bus.js +5 -1
  79. package/packages/@monomind/cli/dist/src/orgrt/daemon.d.ts +46 -0
  80. package/packages/@monomind/cli/dist/src/orgrt/daemon.js +201 -18
  81. package/packages/@monomind/cli/dist/src/orgrt/forwarder.d.ts +10 -5
  82. package/packages/@monomind/cli/dist/src/orgrt/forwarder.js +138 -8
  83. package/packages/@monomind/cli/dist/src/orgrt/inbox.d.ts +11 -0
  84. package/packages/@monomind/cli/dist/src/orgrt/inbox.js +56 -0
  85. package/packages/@monomind/cli/dist/src/orgrt/policy.d.ts +5 -1
  86. package/packages/@monomind/cli/dist/src/orgrt/policy.js +58 -5
  87. package/packages/@monomind/cli/dist/src/orgrt/provider.js +1 -0
  88. package/packages/@monomind/cli/dist/src/orgrt/scheduler.d.ts +13 -0
  89. package/packages/@monomind/cli/dist/src/orgrt/scheduler.js +48 -0
  90. package/packages/@monomind/cli/dist/src/orgrt/server.d.ts +9 -0
  91. package/packages/@monomind/cli/dist/src/orgrt/server.js +39 -0
  92. package/packages/@monomind/cli/dist/src/orgrt/session.d.ts +10 -1
  93. package/packages/@monomind/cli/dist/src/orgrt/session.js +24 -1
  94. package/packages/@monomind/cli/dist/src/orgrt/test-loop.d.ts +12 -0
  95. package/packages/@monomind/cli/dist/src/orgrt/test-loop.js +108 -0
  96. package/packages/@monomind/cli/dist/src/orgrt/types.d.ts +52 -52
  97. package/packages/@monomind/cli/dist/src/output.d.ts +29 -29
  98. package/packages/@monomind/cli/dist/src/output.js +22 -7
  99. package/packages/@monomind/cli/dist/src/parser.d.ts +32 -0
  100. package/packages/@monomind/cli/dist/src/parser.js +130 -5
  101. package/packages/@monomind/cli/dist/src/routing/embed-worker.js +13 -1
  102. package/packages/@monomind/cli/dist/src/routing/route-layer-factory.js +31 -0
  103. package/packages/@monomind/cli/dist/src/services/config-file-manager.d.ts +21 -0
  104. package/packages/@monomind/cli/dist/src/services/config-file-manager.js +60 -7
  105. package/packages/@monomind/cli/dist/src/services/crash-reporter.js +2 -1
  106. package/packages/@monomind/cli/dist/src/types.d.ts +2 -2
  107. package/packages/@monomind/cli/dist/src/ui/dashboard.html +243 -49
  108. package/packages/@monomind/cli/dist/src/ui/orgs-files.js +192 -0
  109. package/packages/@monomind/cli/dist/src/ui/orgs.html +79 -11
  110. package/packages/@monomind/cli/dist/src/ui/server.mjs +346 -139
  111. package/packages/@monomind/cli/dist/src/utils/input-guards.d.ts +9 -0
  112. package/packages/@monomind/cli/dist/src/utils/input-guards.js +39 -5
  113. package/packages/@monomind/cli/package.json +15 -15
@@ -77,9 +77,12 @@ export class OutputFormatter {
77
77
  // Check for NO_COLOR environment variable
78
78
  if (process.env.NO_COLOR !== undefined)
79
79
  return false;
80
- // Check for FORCE_COLOR environment variable
81
- if (process.env.FORCE_COLOR !== undefined)
82
- return true;
80
+ // Check for FORCE_COLOR environment variable.
81
+ // Per the supports-color convention, FORCE_COLOR=0 (or "false") forces
82
+ // color OFF; any other defined, non-empty value forces color ON.
83
+ if (process.env.FORCE_COLOR !== undefined) {
84
+ return process.env.FORCE_COLOR !== '0' && process.env.FORCE_COLOR !== 'false';
85
+ }
83
86
  // Check if stdout is a TTY
84
87
  return process.stdout.isTTY ?? false;
85
88
  }
@@ -149,14 +152,18 @@ export class OutputFormatter {
149
152
  if (this.verbosity === 'quiet')
150
153
  return;
151
154
  const icon = this.color('[WARN]', 'yellow', 'bold');
152
- this.writeln(`${icon} ${message}`);
155
+ // Write to stderr (not stdout) so it never interleaves with structured
156
+ // stdout output (e.g. --format json | jq .)
157
+ this.writeErrorln(`${icon} ${message}`);
153
158
  }
154
159
  printInfo(message) {
155
160
  // Info suppressed in quiet mode
156
161
  if (this.verbosity === 'quiet')
157
162
  return;
158
163
  const icon = this.color('[INFO]', 'blue', 'bold');
159
- this.writeln(`${icon} ${message}`);
164
+ // Write to stderr (not stdout) so it never interleaves with structured
165
+ // stdout output (e.g. --format json | jq .)
166
+ this.writeErrorln(`${icon} ${message}`);
160
167
  }
161
168
  printDebug(message) {
162
169
  // Debug only shows in verbose/debug mode
@@ -477,6 +484,14 @@ export class Spinner {
477
484
  start() {
478
485
  if (this.interval)
479
486
  return;
487
+ // `\r`-based in-place redraw only works on a real TTY. Without one
488
+ // (piped output, CI logs, non-interactive terminals) each animation
489
+ // frame prints as its own line instead of overwriting the last —
490
+ // so print the text once and skip the animation entirely.
491
+ if (!process.stdout.isTTY) {
492
+ process.stdout.write(`${this.text}\n`);
493
+ return;
494
+ }
480
495
  this.interval = setInterval(() => {
481
496
  this.render();
482
497
  this.frameIndex = (this.frameIndex + 1) % this.frames.length;
@@ -488,9 +503,9 @@ export class Spinner {
488
503
  if (this.interval) {
489
504
  clearInterval(this.interval);
490
505
  this.interval = null;
506
+ // Clear the line (only meaningful when the animation actually ran on a TTY)
507
+ process.stdout.write('\r' + ' '.repeat(this.text.length + 10) + '\r');
491
508
  }
492
- // Clear the line
493
- process.stdout.write('\r' + ' '.repeat(this.text.length + 10) + '\r');
494
509
  if (message) {
495
510
  this.formatter.writeln(message);
496
511
  }
@@ -35,7 +35,35 @@ export declare class CommandParser {
35
35
  */
36
36
  private static readonly RESERVED_FLAG_KEYS;
37
37
  private setFlagSafe;
38
+ /**
39
+ * Merge a single parsed flag (or set of `_` positionals) into the
40
+ * accumulated result flags, collecting repeats into an array instead of
41
+ * overwriting. Declared `type: 'array'` options always end up as an array
42
+ * (even a single occurrence); any other repeated flag also becomes an
43
+ * array on its second occurrence rather than silently dropping the first
44
+ * value.
45
+ */
46
+ private mergeParsedFlags;
38
47
  parse(args: string[]): ParseResult;
48
+ /**
49
+ * Convert a camelCase key to kebab-case (inverse of normalizeKey).
50
+ */
51
+ private camelToKebab;
52
+ /**
53
+ * Ensure every flag is reachable under both its camelCase and
54
+ * kebab-case spelling. Runs once, after all flags (including
55
+ * defaults) have been merged into the result.
56
+ */
57
+ private mirrorFlagKeys;
58
+ /**
59
+ * True when `value` looks like a space-separated negative number
60
+ * (`-0.5`, `-42`) rather than a new flag. Scoped narrowly to "leading `-`
61
+ * immediately followed by a digit" — a legitimate flag name never starts
62
+ * with a digit, so this can't misfire on a genuine following flag while
63
+ * still letting `--threshold -0.5` consume `-0.5` as the value instead of
64
+ * being misparsed as a new (bogus) flag.
65
+ */
66
+ private looksLikeNegativeNumber;
39
67
  private parseFlag;
40
68
  private parseValue;
41
69
  private normalizeKey;
@@ -50,6 +78,10 @@ export declare class CommandParser {
50
78
  * Get boolean flags scoped to a specific command/subcommand.
51
79
  */
52
80
  private getScopedBooleanFlags;
81
+ /**
82
+ * Get flags declared `type: 'array'`, scoped to a specific command/subcommand.
83
+ */
84
+ private getScopedArrayFlags;
53
85
  private getBooleanFlags;
54
86
  private applyDefaults;
55
87
  validateFlags(flags: ParsedFlags, command?: Command): string[];
@@ -72,6 +72,12 @@ export class CommandParser {
72
72
  description: 'Enable interactive mode',
73
73
  type: 'boolean',
74
74
  default: true
75
+ },
76
+ {
77
+ name: 'update',
78
+ description: 'Check for updates on startup (use --no-update to disable)',
79
+ type: 'boolean',
80
+ default: true
75
81
  }
76
82
  ];
77
83
  }
@@ -111,6 +117,48 @@ export class CommandParser {
111
117
  return;
112
118
  flags[key] = value;
113
119
  }
120
+ /**
121
+ * Merge a single parsed flag (or set of `_` positionals) into the
122
+ * accumulated result flags, collecting repeats into an array instead of
123
+ * overwriting. Declared `type: 'array'` options always end up as an array
124
+ * (even a single occurrence); any other repeated flag also becomes an
125
+ * array on its second occurrence rather than silently dropping the first
126
+ * value.
127
+ */
128
+ mergeParsedFlags(into, from, arrayFlags, booleanFlags) {
129
+ for (const key of Object.keys(from)) {
130
+ if (key === '_') {
131
+ into._.push(...from._);
132
+ continue;
133
+ }
134
+ if (CommandParser.RESERVED_FLAG_KEYS.has(key))
135
+ continue;
136
+ const incoming = from[key];
137
+ if (Array.isArray(into[key])) {
138
+ into[key].push(...[].concat(incoming).map(String));
139
+ }
140
+ else if (into[key] !== undefined && booleanFlags?.has(key)) {
141
+ // Declared boolean flag repeated (e.g. `--dry-run --dry-run`) — last
142
+ // value wins instead of promoting to an array. Callers throughout the
143
+ // codebase check booleans with `flags['x'] === true`; an array value
144
+ // would silently fail that check (e.g. guidance.ts's --dry-run,
145
+ // which guards a destructive write).
146
+ into[key] = incoming;
147
+ }
148
+ else if (into[key] !== undefined) {
149
+ // Repeated flag not previously an array — promote to array so the
150
+ // earlier value isn't lost.
151
+ into[key] = [String(into[key]), String(incoming)];
152
+ }
153
+ else if (arrayFlags.has(key)) {
154
+ // First occurrence of a declared array flag — still wrap in an array.
155
+ into[key] = [String(incoming)];
156
+ }
157
+ else {
158
+ into[key] = incoming;
159
+ }
160
+ }
161
+ }
114
162
  parse(args) {
115
163
  const result = {
116
164
  command: [],
@@ -135,6 +183,7 @@ export class CommandParser {
135
183
  // Subcommand-specific aliases take priority over global ones
136
184
  const aliases = this.buildScopedAliases(resolvedSub || resolvedCmd);
137
185
  const booleanFlags = this.getScopedBooleanFlags(resolvedSub || resolvedCmd);
186
+ const arrayFlags = this.getScopedArrayFlags(resolvedSub || resolvedCmd);
138
187
  let i = 0;
139
188
  let parsingFlags = true;
140
189
  while (i < args.length) {
@@ -148,8 +197,13 @@ export class CommandParser {
148
197
  // Handle flags
149
198
  if (parsingFlags && arg.startsWith('-')) {
150
199
  const parseResult = this.parseFlag(args, i, aliases, booleanFlags);
151
- // Apply to result flags
152
- Object.assign(result.flags, parseResult.flags);
200
+ // Merge into result flags. A plain Object.assign here would silently
201
+ // drop earlier values whenever the same flag is passed more than once
202
+ // (`-p a=1 -p b=2` kept only `b=2`) — options declared `type: 'array'`
203
+ // (e.g. browse-action.ts's --params/-p, swarm.ts) are meant to
204
+ // collect every occurrence, and even a flag NOT declared array
205
+ // shouldn't silently lose data on repetition.
206
+ this.mergeParsedFlags(result.flags, parseResult.flags, arrayFlags, booleanFlags);
153
207
  i = parseResult.nextIndex;
154
208
  continue;
155
209
  }
@@ -201,8 +255,55 @@ export class CommandParser {
201
255
  // shadow same-name global options (e.g. `browse screenshot --format` is an
202
256
  // image format, not the global text|json|table output format).
203
257
  this.applyDefaults(result.flags, resolvedCmd);
258
+ // Dual-write flag keys: every flag ends up stored under BOTH its
259
+ // camelCase form (`keepConfig`) and its original kebab-case form
260
+ // (`keep-config`). Historically only the camelCase key was ever set,
261
+ // but dozens of command action functions across the codebase read
262
+ // `ctx.flags['kebab-case']` directly — which was always `undefined`,
263
+ // silently disabling those flags (see AUDIT-BACKLOG P0-10). Running
264
+ // this as a single pass over the fully-merged flags object (long
265
+ // flags, `--no-x` negation, short-flag aliases, and defaults are all
266
+ // merged into `result.flags` by this point) covers every entry point
267
+ // in one place instead of touching each call site individually.
268
+ this.mirrorFlagKeys(result.flags);
204
269
  return result;
205
270
  }
271
+ /**
272
+ * Convert a camelCase key to kebab-case (inverse of normalizeKey).
273
+ */
274
+ camelToKebab(key) {
275
+ return key.replace(/[A-Z]/g, (letter) => '-' + letter.toLowerCase());
276
+ }
277
+ /**
278
+ * Ensure every flag is reachable under both its camelCase and
279
+ * kebab-case spelling. Runs once, after all flags (including
280
+ * defaults) have been merged into the result.
281
+ */
282
+ mirrorFlagKeys(flags) {
283
+ for (const key of Object.keys(flags)) {
284
+ if (key === '_' || CommandParser.RESERVED_FLAG_KEYS.has(key))
285
+ continue;
286
+ const camelKey = this.normalizeKey(key);
287
+ if (camelKey !== key && flags[camelKey] === undefined && !CommandParser.RESERVED_FLAG_KEYS.has(camelKey)) {
288
+ flags[camelKey] = flags[key];
289
+ }
290
+ const kebabKey = this.camelToKebab(key);
291
+ if (kebabKey !== key && flags[kebabKey] === undefined && !CommandParser.RESERVED_FLAG_KEYS.has(kebabKey)) {
292
+ flags[kebabKey] = flags[key];
293
+ }
294
+ }
295
+ }
296
+ /**
297
+ * True when `value` looks like a space-separated negative number
298
+ * (`-0.5`, `-42`) rather than a new flag. Scoped narrowly to "leading `-`
299
+ * immediately followed by a digit" — a legitimate flag name never starts
300
+ * with a digit, so this can't misfire on a genuine following flag while
301
+ * still letting `--threshold -0.5` consume `-0.5` as the value instead of
302
+ * being misparsed as a new (bogus) flag.
303
+ */
304
+ looksLikeNegativeNumber(value) {
305
+ return /^-\d/.test(value);
306
+ }
206
307
  parseFlag(args, index, aliases, booleanFlags) {
207
308
  const flags = { _: [] };
208
309
  const arg = args[index];
@@ -239,7 +340,8 @@ export class CommandParser {
239
340
  if (booleanFlags.has(normalizedKey)) {
240
341
  this.setFlagSafe(flags, normalizedKey, true);
241
342
  }
242
- else if (nextIndex < args.length && !args[nextIndex].startsWith('-')) {
343
+ else if (nextIndex < args.length &&
344
+ (!args[nextIndex].startsWith('-') || this.looksLikeNegativeNumber(args[nextIndex]))) {
243
345
  this.setFlagSafe(flags, normalizedKey, this.parseValue(args[nextIndex]));
244
346
  nextIndex++;
245
347
  }
@@ -258,7 +360,8 @@ export class CommandParser {
258
360
  if (booleanFlags.has(normalizedKey)) {
259
361
  this.setFlagSafe(flags, normalizedKey, true);
260
362
  }
261
- else if (nextIndex < args.length && !args[nextIndex].startsWith('-')) {
363
+ else if (nextIndex < args.length &&
364
+ (!args[nextIndex].startsWith('-') || this.looksLikeNegativeNumber(args[nextIndex]))) {
262
365
  this.setFlagSafe(flags, normalizedKey, this.parseValue(args[nextIndex]));
263
366
  nextIndex++;
264
367
  }
@@ -368,6 +471,20 @@ export class CommandParser {
368
471
  }
369
472
  return flags;
370
473
  }
474
+ /**
475
+ * Get flags declared `type: 'array'`, scoped to a specific command/subcommand.
476
+ */
477
+ getScopedArrayFlags(resolvedCmd) {
478
+ const flags = new Set();
479
+ if (resolvedCmd?.options) {
480
+ for (const opt of resolvedCmd.options) {
481
+ if (opt.type === 'array') {
482
+ flags.add(this.normalizeKey(opt.name));
483
+ }
484
+ }
485
+ }
486
+ return flags;
487
+ }
371
488
  getBooleanFlags() {
372
489
  const flags = new Set();
373
490
  for (const opt of this.globalOptions) {
@@ -471,7 +588,15 @@ export class CommandParser {
471
588
  }
472
589
  // Check for unknown flags if not allowed
473
590
  if (!this.options.allowUnknownFlags) {
474
- const knownFlags = new Set(allOptions.map(opt => this.normalizeKey(opt.name)));
591
+ // Include both the camelCase and original (kebab-case) spelling of
592
+ // each option name — parse() now mirrors flags under both forms, so
593
+ // validation must recognize both or the mirrored key would be
594
+ // flagged as an unknown option.
595
+ const knownFlags = new Set();
596
+ for (const opt of allOptions) {
597
+ knownFlags.add(this.normalizeKey(opt.name));
598
+ knownFlags.add(opt.name);
599
+ }
475
600
  knownFlags.add('_'); // Positional args
476
601
  for (const key of Object.keys(flags)) {
477
602
  if (!knownFlags.has(key) && key !== '_') {
@@ -50,7 +50,19 @@ async function main() {
50
50
  const result = await layer.route(task);
51
51
  // Emit on its own line, prefixed, so the parent can unambiguously locate the
52
52
  // result even if the model loader wrote stray bytes to stdout earlier.
53
- process.stdout.write(`\n__ROUTE_RESULT__${JSON.stringify(result)}\n`);
53
+ //
54
+ // Fix (P1-3): onnxruntime's native thread pool keeps the event loop alive
55
+ // even after this function returns, so the process never exits on its own
56
+ // and the parent's `close` listener waits until it force-kills us at the
57
+ // timeout — discarding the result we already wrote. Force-exit once the
58
+ // write is flushed (same onnx-teardown class as memory.ts's #1428 fix).
59
+ const wroteSynchronously = process.stdout.write(`\n__ROUTE_RESULT__${JSON.stringify(result)}\n`);
60
+ if (wroteSynchronously) {
61
+ process.exit(0);
62
+ }
63
+ else {
64
+ process.stdout.once('drain', () => process.exit(0));
65
+ }
54
66
  }
55
67
  main().catch((err) => {
56
68
  process.stderr.write(`embed-worker: ${err instanceof Error ? err.message : String(err)}\n`);
@@ -56,6 +56,35 @@ function runWorker(task) {
56
56
  reject(new Error('embed worker timed out'));
57
57
  }, WORKER_TIMEOUT_MS);
58
58
  timer.unref?.();
59
+ // Resolve as soon as the result marker is *seen*, rather than waiting for
60
+ // the child's `close` event. This is a robustness net: onnxruntime's
61
+ // native thread pool can keep the child's event loop alive after it has
62
+ // already written its result, so `close` may never fire (or only fires
63
+ // after a slow OS-level teardown). embed-worker.ts explicitly
64
+ // process.exit()s right after writing the marker, but resolving here too
65
+ // means a future regression in that exit call degrades to "resolves
66
+ // immediately, child lingers" instead of "silently discards a correct
67
+ // result after a 90s timeout".
68
+ const tryResolveFromMarker = () => {
69
+ if (settled)
70
+ return;
71
+ const markedLine = stdout
72
+ .split('\n')
73
+ .reverse()
74
+ .find((l) => l.startsWith(RESULT_MARKER));
75
+ if (!markedLine)
76
+ return;
77
+ try {
78
+ const parsed = JSON.parse(markedLine.slice(RESULT_MARKER.length));
79
+ settled = true;
80
+ clearTimeout(timer);
81
+ resolve(parsed);
82
+ }
83
+ catch {
84
+ // Marker line present but not yet valid JSON (unlikely — it's written
85
+ // in one shot) — fall through and let `close`/timeout handle it.
86
+ }
87
+ };
59
88
  child.stdout?.on('data', (d) => {
60
89
  stdout += d.toString();
61
90
  if (stdout.length > MAX_WORKER_OUTPUT) {
@@ -68,7 +97,9 @@ function runWorker(task) {
68
97
  }
69
98
  catch { /* already dead */ }
70
99
  reject(new Error('embed worker produced excessive output'));
100
+ return;
71
101
  }
102
+ tryResolveFromMarker();
72
103
  });
73
104
  child.stderr?.on('data', (d) => { if (stderr.length < MAX_WORKER_OUTPUT)
74
105
  stderr += d.toString(); });
@@ -2,11 +2,32 @@
2
2
  * Config File Manager
3
3
  * Shared JSON config file persistence with atomic writes and Zod validation
4
4
  */
5
+ /**
6
+ * Default config values.
7
+ *
8
+ * IMPORTANT: never hand this object (or a shallow `{ ...DEFAULT_CONFIG }`
9
+ * copy) out directly — its nested sections (`agents`, `swarm`, `memory`, ...)
10
+ * would be shared-by-reference with every caller, so one caller mutating
11
+ * `config.swarm.maxAgents` would silently corrupt this module-level constant
12
+ * for the rest of the process. Always hand out `cloneDefaultConfig()`.
13
+ */
14
+ declare const DEFAULT_CONFIG: Record<string, unknown>;
15
+ /** Exposed read-only for callers (e.g. config-adapter.ts) that need to
16
+ * agree with this module's defaults instead of hardcoding their own. */
17
+ export { DEFAULT_CONFIG };
5
18
  export declare class ConfigFileManager {
6
19
  private configPath;
7
20
  private config;
8
21
  /** Find config file in search paths starting from cwd */
9
22
  findConfig(cwd: string): string | null;
23
+ /**
24
+ * Load config from an EXACT file path (used by `--config <file>` / `-c`).
25
+ * Unlike load()/findConfig(), this never falls back to directory search —
26
+ * an explicitly-named config file that doesn't exist or fails to parse is
27
+ * an error, not a silent fallback to defaults or an unrelated file in the
28
+ * same directory.
29
+ */
30
+ loadExact(filePath: string): Record<string, unknown>;
10
31
  /** Load config from file, returns null if not found */
11
32
  load(cwd: string): Record<string, unknown> | null;
12
33
  /** Get the current config, loading if needed */
@@ -10,7 +10,15 @@ const CONFIG_FILENAMES = [
10
10
  'monomind.config.json',
11
11
  '.monomind/config.json',
12
12
  ];
13
- /** Default config values */
13
+ /**
14
+ * Default config values.
15
+ *
16
+ * IMPORTANT: never hand this object (or a shallow `{ ...DEFAULT_CONFIG }`
17
+ * copy) out directly — its nested sections (`agents`, `swarm`, `memory`, ...)
18
+ * would be shared-by-reference with every caller, so one caller mutating
19
+ * `config.swarm.maxAgents` would silently corrupt this module-level constant
20
+ * for the rest of the process. Always hand out `cloneDefaultConfig()`.
21
+ */
14
22
  const DEFAULT_CONFIG = {
15
23
  version: '3.5',
16
24
  agents: {
@@ -61,6 +69,20 @@ const DEFAULT_CONFIG = {
61
69
  },
62
70
  },
63
71
  };
72
+ /**
73
+ * Deep-clone the default config so every caller gets its own independent
74
+ * object graph. `{ ...DEFAULT_CONFIG }` is only a shallow copy — its nested
75
+ * sub-configs (`agents`, `swarm`, `memory`, ...) would remain the SAME
76
+ * object references as the module-level constant, so mutating a nested
77
+ * field on one caller's copy would corrupt the shared default for every
78
+ * subsequent caller in the process.
79
+ */
80
+ function cloneDefaultConfig() {
81
+ return structuredClone(DEFAULT_CONFIG);
82
+ }
83
+ /** Exposed read-only for callers (e.g. config-adapter.ts) that need to
84
+ * agree with this module's defaults instead of hardcoding their own. */
85
+ export { DEFAULT_CONFIG };
64
86
  export class ConfigFileManager {
65
87
  configPath = null;
66
88
  config = null;
@@ -83,6 +105,36 @@ export class ConfigFileManager {
83
105
  }
84
106
  return null;
85
107
  }
108
+ /**
109
+ * Load config from an EXACT file path (used by `--config <file>` / `-c`).
110
+ * Unlike load()/findConfig(), this never falls back to directory search —
111
+ * an explicitly-named config file that doesn't exist or fails to parse is
112
+ * an error, not a silent fallback to defaults or an unrelated file in the
113
+ * same directory.
114
+ */
115
+ loadExact(filePath) {
116
+ const resolved = path.resolve(filePath);
117
+ if (!fs.existsSync(resolved)) {
118
+ throw new Error(`Config file not found: ${resolved}`);
119
+ }
120
+ let content;
121
+ try {
122
+ content = fs.readFileSync(resolved, 'utf-8');
123
+ }
124
+ catch (err) {
125
+ throw new Error(`Failed to read config file ${resolved}: ${err instanceof Error ? err.message : String(err)}`);
126
+ }
127
+ let parsed;
128
+ try {
129
+ parsed = JSON.parse(content);
130
+ }
131
+ catch (err) {
132
+ throw new Error(`Failed to parse config file ${resolved}: ${err instanceof Error ? err.message : String(err)}`);
133
+ }
134
+ this.configPath = resolved;
135
+ this.config = sanitizeConfigObject(parsed);
136
+ return this.config;
137
+ }
86
138
  /** Load config from file, returns null if not found */
87
139
  load(cwd) {
88
140
  this.configPath = this.findConfig(cwd);
@@ -106,7 +158,7 @@ export class ConfigFileManager {
106
158
  if (this.config === null) {
107
159
  this.load(cwd);
108
160
  }
109
- return this.config ?? { ...DEFAULT_CONFIG };
161
+ return this.config ?? cloneDefaultConfig();
110
162
  }
111
163
  /** Get a nested config value by dot-separated key */
112
164
  get(cwd, key) {
@@ -136,7 +188,7 @@ export class ConfigFileManager {
136
188
  // cross-process atomic without an OS-level flock, but combined with the
137
189
  // O_EXCL atomic rename it prevents the most common credential-clobber
138
190
  // window where two CLIs interleave their getConfig→set→write cycles.
139
- let onDisk = { ...DEFAULT_CONFIG };
191
+ let onDisk = cloneDefaultConfig();
140
192
  if (fs.existsSync(targetPath)) {
141
193
  try {
142
194
  const parsed = JSON.parse(fs.readFileSync(targetPath, 'utf-8'));
@@ -155,7 +207,7 @@ export class ConfigFileManager {
155
207
  if (fs.existsSync(targetPath) && !force) {
156
208
  throw new Error(`Config file already exists: ${targetPath}. Use --force to overwrite.`);
157
209
  }
158
- const config = { ...DEFAULT_CONFIG, ...(overrides ? sanitizeConfigObject(overrides) : {}) };
210
+ const config = { ...cloneDefaultConfig(), ...(overrides ? sanitizeConfigObject(overrides) : {}) };
159
211
  this.writeAtomic(targetPath, config);
160
212
  this.config = config;
161
213
  this.configPath = targetPath;
@@ -164,8 +216,9 @@ export class ConfigFileManager {
164
216
  /** Reset config to defaults */
165
217
  reset(cwd) {
166
218
  const targetPath = this.configPath ?? path.resolve(cwd, CONFIG_FILENAMES[0]);
167
- this.writeAtomic(targetPath, DEFAULT_CONFIG);
168
- this.config = { ...DEFAULT_CONFIG };
219
+ const fresh = cloneDefaultConfig();
220
+ this.writeAtomic(targetPath, fresh);
221
+ this.config = fresh;
169
222
  this.configPath = targetPath;
170
223
  return targetPath;
171
224
  }
@@ -228,7 +281,7 @@ export class ConfigFileManager {
228
281
  }
229
282
  /** Get default config */
230
283
  getDefaults() {
231
- return { ...DEFAULT_CONFIG };
284
+ return cloneDefaultConfig();
232
285
  }
233
286
  /** Atomic write with restrictive 0o600 mode.
234
287
  * SECURITY: this config file may contain API keys (per `commands/providers.ts`).
@@ -84,7 +84,8 @@ export function redact(text) {
84
84
  /(?:secret|password|passwd|pwd)\s*[:=]\s*['"]?[^\s'"]{8,}['"]?/gi,
85
85
  /(?:token|bearer)\s*[:=]\s*['"]?[^\s'"]{10,}['"]?/gi,
86
86
  /-----BEGIN (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----/g,
87
- /sk-[a-zA-Z0-9]{20,}/g,
87
+ /sk-ant-[a-zA-Z0-9_-]{20,}/g,
88
+ /sk-[a-zA-Z0-9_-]{20,}/g,
88
89
  /ghp_[a-zA-Z0-9]{36}/g,
89
90
  /gho_[a-zA-Z0-9]{36}/g,
90
91
  /npm_[a-zA-Z0-9]{36}/g,
@@ -184,7 +184,7 @@ export interface MultiSelectPromptOptions<T = string> {
184
184
  export declare class CLIError extends Error {
185
185
  code: string;
186
186
  exitCode: number;
187
- details?: unknown | undefined;
188
- constructor(message: string, code: string, exitCode?: number, details?: unknown | undefined);
187
+ details?: unknown;
188
+ constructor(message: string, code: string, exitCode?: number, details?: unknown);
189
189
  }
190
190
  //# sourceMappingURL=types.d.ts.map