swarmdo 1.29.0 → 1.37.0

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 (76) hide show
  1. package/.claude/helpers/auto-memory-hook.mjs +66 -33
  2. package/README.md +6 -3
  3. package/package.json +1 -1
  4. package/v3/@swarmdo/cli/.claude/helpers/auto-memory-hook.mjs +65 -32
  5. package/v3/@swarmdo/cli/dist/src/affected/affected.d.ts +6 -1
  6. package/v3/@swarmdo/cli/dist/src/affected/affected.js +7 -2
  7. package/v3/@swarmdo/cli/dist/src/apply/apply.js +18 -7
  8. package/v3/@swarmdo/cli/dist/src/changelog/changelog.js +4 -1
  9. package/v3/@swarmdo/cli/dist/src/codegraph/codegraph.js +11 -1
  10. package/v3/@swarmdo/cli/dist/src/commands/comms.d.ts +16 -0
  11. package/v3/@swarmdo/cli/dist/src/commands/comms.js +280 -0
  12. package/v3/@swarmdo/cli/dist/src/commands/compact.js +6 -2
  13. package/v3/@swarmdo/cli/dist/src/commands/config.js +1 -0
  14. package/v3/@swarmdo/cli/dist/src/commands/hooks.js +121 -1
  15. package/v3/@swarmdo/cli/dist/src/commands/hotspots.js +8 -2
  16. package/v3/@swarmdo/cli/dist/src/commands/index.js +6 -0
  17. package/v3/@swarmdo/cli/dist/src/commands/permissions.d.ts +14 -0
  18. package/v3/@swarmdo/cli/dist/src/commands/permissions.js +92 -0
  19. package/v3/@swarmdo/cli/dist/src/commands/usage.js +208 -3
  20. package/v3/@swarmdo/cli/dist/src/comms/mailbox.d.ts +88 -0
  21. package/v3/@swarmdo/cli/dist/src/comms/mailbox.js +138 -0
  22. package/v3/@swarmdo/cli/dist/src/comms/store.d.ts +21 -0
  23. package/v3/@swarmdo/cli/dist/src/comms/store.js +44 -0
  24. package/v3/@swarmdo/cli/dist/src/compact/compact.js +4 -1
  25. package/v3/@swarmdo/cli/dist/src/compact-snapshot/compact-snapshot.js +8 -3
  26. package/v3/@swarmdo/cli/dist/src/config-lint/lint.d.ts +3 -1
  27. package/v3/@swarmdo/cli/dist/src/config-lint/lint.js +11 -4
  28. package/v3/@swarmdo/cli/dist/src/env/env.js +40 -14
  29. package/v3/@swarmdo/cli/dist/src/hooks-recipe/recipe.js +20 -1
  30. package/v3/@swarmdo/cli/dist/src/hotspots/hotspots.d.ts +3 -0
  31. package/v3/@swarmdo/cli/dist/src/hotspots/hotspots.js +9 -0
  32. package/v3/@swarmdo/cli/dist/src/index.js +8 -0
  33. package/v3/@swarmdo/cli/dist/src/license/license.js +4 -1
  34. package/v3/@swarmdo/cli/dist/src/mcp-client.js +2 -0
  35. package/v3/@swarmdo/cli/dist/src/mcp-tools/comms-tools.d.ts +11 -0
  36. package/v3/@swarmdo/cli/dist/src/mcp-tools/comms-tools.js +93 -0
  37. package/v3/@swarmdo/cli/dist/src/mcp-tools/index.d.ts +1 -0
  38. package/v3/@swarmdo/cli/dist/src/mcp-tools/index.js +1 -0
  39. package/v3/@swarmdo/cli/dist/src/mcp-tools/task-tools.js +4 -0
  40. package/v3/@swarmdo/cli/dist/src/memory/memory-bridge.d.ts +2 -0
  41. package/v3/@swarmdo/cli/dist/src/memory/memory-bridge.js +4 -2
  42. package/v3/@swarmdo/cli/dist/src/memory-inject/select.d.ts +82 -0
  43. package/v3/@swarmdo/cli/dist/src/memory-inject/select.js +164 -0
  44. package/v3/@swarmdo/cli/dist/src/memory-vault/import.js +11 -1
  45. package/v3/@swarmdo/cli/dist/src/pack/pack.js +6 -2
  46. package/v3/@swarmdo/cli/dist/src/permissions/audit.d.ts +50 -0
  47. package/v3/@swarmdo/cli/dist/src/permissions/audit.js +127 -0
  48. package/v3/@swarmdo/cli/dist/src/plugins/store/search.js +1 -1
  49. package/v3/@swarmdo/cli/dist/src/redact/redact.js +7 -2
  50. package/v3/@swarmdo/cli/dist/src/release/release.js +1 -1
  51. package/v3/@swarmdo/cli/dist/src/sbom/sbom.d.ts +2 -0
  52. package/v3/@swarmdo/cli/dist/src/sbom/sbom.js +5 -0
  53. package/v3/@swarmdo/cli/dist/src/swarmvector/semantic-router.js +8 -3
  54. package/v3/@swarmdo/cli/dist/src/testreport/testreport.js +9 -2
  55. package/v3/@swarmdo/cli/dist/src/transcript/export.js +8 -4
  56. package/v3/@swarmdo/cli/dist/src/update/checker.d.ts +1 -0
  57. package/v3/@swarmdo/cli/dist/src/update/checker.js +7 -2
  58. package/v3/@swarmdo/cli/dist/src/usage/limits.d.ts +77 -0
  59. package/v3/@swarmdo/cli/dist/src/usage/limits.js +134 -0
  60. package/v3/@swarmdo/cli/dist/src/usage/model-efficiency.d.ts +29 -0
  61. package/v3/@swarmdo/cli/dist/src/usage/model-efficiency.js +28 -0
  62. package/v3/@swarmdo/cli/dist/src/usage/reflect-html.d.ts +23 -0
  63. package/v3/@swarmdo/cli/dist/src/usage/reflect-html.js +95 -0
  64. package/v3/@swarmdo/cli/dist/src/usage/reflect.d.ts +131 -0
  65. package/v3/@swarmdo/cli/dist/src/usage/reflect.js +209 -0
  66. package/v3/@swarmdo/cli/dist/src/usage/spend-forecast.d.ts +28 -0
  67. package/v3/@swarmdo/cli/dist/src/usage/spend-forecast.js +33 -0
  68. package/v3/@swarmdo/cli/dist/src/usage/transcript-errors.d.ts +14 -0
  69. package/v3/@swarmdo/cli/dist/src/usage/transcript-errors.js +10 -0
  70. package/v3/@swarmdo/cli/dist/src/usage/transcript-usage.d.ts +8 -1
  71. package/v3/@swarmdo/cli/dist/src/usage/transcript-usage.js +21 -1
  72. package/v3/@swarmdo/cli/dist/src/util/csv.d.ts +14 -0
  73. package/v3/@swarmdo/cli/dist/src/util/csv.js +20 -0
  74. package/v3/@swarmdo/cli/dist/src/util/stdout.d.ts +23 -1
  75. package/v3/@swarmdo/cli/dist/src/util/stdout.js +34 -2
  76. package/v3/@swarmdo/cli/package.json +1 -1
@@ -13,7 +13,7 @@
13
13
 
14
14
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
15
15
  import { join, dirname } from 'path';
16
- import { fileURLToPath } from 'url';
16
+ import { fileURLToPath, pathToFileURL } from 'url';
17
17
 
18
18
  const __filename = fileURLToPath(import.meta.url);
19
19
  const __dirname = dirname(__filename);
@@ -52,13 +52,30 @@ async function gracefulExit(signal) {
52
52
  process.on('SIGTERM', () => { gracefulExit('SIGTERM'); });
53
53
  process.on('SIGINT', () => { gracefulExit('SIGINT'); });
54
54
 
55
- // Ensure data dir
56
- if (!existsSync(DATA_DIR)) mkdirSync(DATA_DIR, { recursive: true });
57
-
58
55
  // ============================================================================
59
56
  // Simple JSON File Backend (implements IMemoryBackend interface)
60
57
  // ============================================================================
61
58
 
59
+ // Collapse entries that are the SAME memory — identical content signature but
60
+ // distinct ids — keeping the first occurrence. The auto-memory bridge
61
+ // historically re-imported every session with a fresh random id, bloating the
62
+ // store ~22x (#53); this heals already-bloated stores and de-dupes defensively.
63
+ // Keyed by namespace + content hash (or raw content) so genuinely distinct
64
+ // memories are never merged. Pure.
65
+ function dedupeByContentSignature(entries) {
66
+ const seen = new Set();
67
+ const out = [];
68
+ for (const e of entries) {
69
+ const ns = (e && e.namespace) || 'default';
70
+ const body = (e && e.metadata && e.metadata.contentHash) || (e && (e.content || e.value)) || '';
71
+ const sig = ns + '\u0001' + body;
72
+ if (seen.has(sig)) continue;
73
+ seen.add(sig);
74
+ out.push(e);
75
+ }
76
+ return out;
77
+ }
78
+
62
79
  class JsonFileBackend {
63
80
  constructor(filePath) {
64
81
  this.filePath = filePath;
@@ -70,7 +87,12 @@ class JsonFileBackend {
70
87
  try {
71
88
  const data = JSON.parse(readFileSync(this.filePath, 'utf-8'));
72
89
  if (Array.isArray(data)) {
73
- for (const entry of data) this.entries.set(entry.id, entry);
90
+ // Collapse content-duplicates left by the pre-#53 import bug (same
91
+ // memory re-imported each session under a fresh id). Auto-heals an
92
+ // already-bloated store by rewriting it once when dupes are dropped.
93
+ const deduped = dedupeByContentSignature(data);
94
+ for (const entry of deduped) this.entries.set(entry.id, entry);
95
+ if (deduped.length < data.length) this._persist();
74
96
  }
75
97
  } catch { /* start fresh */ }
76
98
  }
@@ -617,33 +639,44 @@ async function doImportAll() {
617
639
  // Main
618
640
  // ============================================================================
619
641
 
620
- const command = process.argv[2] || 'status';
621
-
622
- // Dynamic import() failures can surface as unhandled rejections on a later
623
- // microtask even when the awaiting call site already caught them, which would
624
- // otherwise force a non-zero exit. Swallow to keep hooks exit-0, but surface the
625
- // reason under SWARMDO_DEBUG/DEBUG so genuine async bugs aren't silently hidden
626
- // (FIX 2 — the previous `() => {}` discarded every rejection process-wide).
627
- process.on('unhandledRejection', (reason) => {
628
- if (DEBUG) {
629
- const detail = reason && reason.message ? reason.message : String(reason);
630
- process.stderr.write(`[AutoMemory] unhandledRejection (suppressed): ${detail}\n`);
631
- }
632
- });
633
-
634
- try {
635
- switch (command) {
636
- case 'import': await doImport(); break;
637
- case 'import-all': await doImportAll(); break;
638
- case 'sync': await doSync(); break;
639
- case 'status': await doStatus(); break;
640
- default:
641
- console.log('Usage: auto-memory-hook.mjs <import|sync|status>');
642
- process.exit(1);
642
+ // Only dispatch CLI commands when this file is executed directly
643
+ // (`node auto-memory-hook.mjs …`), NOT when it is imported by a test —
644
+ // importing must not run a command or exit the process (#53 regression test
645
+ // imports JsonFileBackend + dedupeByContentSignature below).
646
+ if (import.meta.url === pathToFileURL(process.argv[1] || '').href) {
647
+ // Ensure data dir (only when actually running a command, not on import)
648
+ if (!existsSync(DATA_DIR)) mkdirSync(DATA_DIR, { recursive: true });
649
+ const command = process.argv[2] || 'status';
650
+
651
+ // Dynamic import() failures can surface as unhandled rejections on a later
652
+ // microtask even when the awaiting call site already caught them, which would
653
+ // otherwise force a non-zero exit. Swallow to keep hooks exit-0, but surface the
654
+ // reason under SWARMDO_DEBUG/DEBUG so genuine async bugs aren't silently hidden
655
+ // (FIX 2 — the previous `() => {}` discarded every rejection process-wide).
656
+ process.on('unhandledRejection', (reason) => {
657
+ if (DEBUG) {
658
+ const detail = reason && reason.message ? reason.message : String(reason);
659
+ process.stderr.write(`[AutoMemory] unhandledRejection (suppressed): ${detail}\n`);
660
+ }
661
+ });
662
+
663
+ try {
664
+ switch (command) {
665
+ case 'import': await doImport(); break;
666
+ case 'import-all': await doImportAll(); break;
667
+ case 'sync': await doSync(); break;
668
+ case 'status': await doStatus(); break;
669
+ default:
670
+ console.log('Usage: auto-memory-hook.mjs <import|sync|status>');
671
+ process.exit(1);
672
+ }
673
+ } catch (err) {
674
+ // Hooks must never crash Claude Code - fail silently
675
+ dim(`Error (non-critical): ${err.message}`);
643
676
  }
644
- } catch (err) {
645
- // Hooks must never crash Claude Code - fail silently
646
- dim(`Error (non-critical): ${err.message}`);
677
+ // Ensure clean exit for Claude Code hooks (exit 0 = success, no stderr = no error)
678
+ process.exit(0);
647
679
  }
648
- // Ensure clean exit for Claude Code hooks (exit 0 = success, no stderr = no error)
649
- process.exit(0);
680
+
681
+ // Exported for unit tests (the run-guard above keeps import side-effect-free).
682
+ export { JsonFileBackend, dedupeByContentSignature };
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![swarmdo](swarmdo/assets/brand/logo-full.svg)](https://swarmdo.com)
4
4
 
5
- [![npm version (swarmdo)](https://img.shields.io/badge/npx%20swarmdo-v1.29.0-cb3837?style=for-the-badge&logo=npm&logoColor=white)](https://github.com/SwarmDo/swarmdo/releases)
5
+ [![npm version (swarmdo)](https://img.shields.io/badge/npx%20swarmdo-v1.37.0-cb3837?style=for-the-badge&logo=npm&logoColor=white)](https://github.com/SwarmDo/swarmdo/releases)
6
6
  [![MIT License](https://img.shields.io/badge/License-MIT-yellow?style=for-the-badge)](https://github.com/SwarmDo/swarmdo/blob/main/LICENSE)
7
7
  [![Website](https://img.shields.io/badge/swarmdo.com-e2a33c?style=for-the-badge&logoColor=black)](https://swarmdo.com)
8
8
  [![Star on GitHub](https://img.shields.io/github/stars/SwarmDo/swarmdo?style=for-the-badge&logo=github&color=gold)](https://github.com/SwarmDo/swarmdo)
@@ -271,7 +271,7 @@ The recent release train added a full day-to-day operations layer around the swa
271
271
 
272
272
  | Command | What it does |
273
273
  |---------|-------------|
274
- | `swarmdo usage` (alias `cost`) | Claude Code **spend analytics** from your local transcripts — `daily`, `monthly`, `models`, `projects`, `sessions`, live 5-hour `blocks` burn, `errors` (tool-failure analytics), `cache` (prompt-cache efficiency + $ saved), and `diff` (period-over-period comparison with per-model movers) |
274
+ | `swarmdo usage` (alias `cost`) | Claude Code **spend analytics** from your local transcripts — `daily`, `weekly`, `monthly` (with a month-end spend projection), `models`, `projects`, `sessions`, live 5-hour `blocks` burn, `errors` (tool-failure analytics), `cache` (prompt-cache efficiency + $ saved), `diff` (period-over-period comparison with per-model movers), `reflect` (a wrapped-style retrospective — busiest day, streak, top models/projects, peak hour, delegation ratio — with a shareable `--html` dashboard), and `limits` (an official-quota **exhaustion forecaster** over Claude Code's `rate_limits`); the standard views take `--csv` for spreadsheet export |
275
275
  | `swarmdo usage guard` | **Budget policy** — limits for the active 5h block / today / month via flags or `SWARMDO_GUARD_*` env → ok / warn / over; `--strict` exits 1, safe for CI gates and Stop hooks |
276
276
  | `swarmdo hud` | **One-screen ops HUD** — 5h block burn, task readiness, daemon workers, memory snapshots (`--watch`, `--json`) |
277
277
  | `swarmdo repair` (alias `tdd-repair`) | **Test-Driven Repair** — a bounded, budget-capped headless `claude` loop that fixes source until a failing test passes; dry-run unless `--confirm` |
@@ -294,8 +294,11 @@ The recent release train added a full day-to-day operations layer around the swa
294
294
  | OpenRouter model pool | **Let swarms pick from any models you configure** — declare tier-mapped OpenRouter models in `swarmdo.config.json`; the router Thompson-samples among them per task and the execution layer dispatches the winner |
295
295
  | `swarmdo changelog` (alias `notes`) | **Release notes from conventional commits** — `--out NOTES.md` feeds `gh release create --notes-file` |
296
296
  | `swarmdo mcp doctor` | **MCP config diagnosis** — missing binaries, bad URLs, malformed entries across `.mcp.json` + `~/.claude.json` |
297
+ | `swarmdo permissions` (alias `perms`) | **Audit your Claude Code permission rules** — static analysis of `permissions.allow`/`deny`/`ask` in `.claude/settings*.json`: flags allow↔deny conflicts (dead rules), over-broad `Bash(*)` grants, shadowed/redundant rules, duplicates, and malformed entries. `--strict` gates CI. Read-only; the static-safety sibling of `config lint` / `mcp doctor` |
298
+ | `swarmdo comms` (alias `mailbox`) | **Cross-session agent mailbox** — one Claude Code session messages another by name (`send -t <session>`, `-t all` broadcasts, `inbox`, `read`, `watch`); sessions on the same repo share `.swarmdo/comms/`. `inbox --hook` surfaces new mail as prompt context without polling. Also MCP tools (`comms_send`/`comms_inbox`) |
299
+ | `swarmdo hooks memory-inject` | **Prompt-time semantic memory injection** — embeds each prompt, vector-searches your stored memories, and injects the most relevant under a token budget (recall at the moment of need); wire it with `hooks recipe memory-inject` |
297
300
  | `swarmdo hooks notify -d` | **Desktop notifications** — OS-native toast (macOS `osascript`, Linux `notify-send`) |
298
- | `swarmdo hooks recipe` | **One-command Claude Code hooks** — "ping me when Claude finishes / needs input"; dry-run by default, idempotent merge that never clobbers your settings |
301
+ | `swarmdo hooks recipe` | **One-command Claude Code hooks** — install `notify-done`/`notify-input` (desktop pings), `memory-inject` (relevant memories each prompt), or `comms-inbox` (new mail as context); dry-run by default, idempotent merge that never clobbers your settings |
299
302
  | `swarmdo preset` + `init --preset` | **5-tier capability ladder** — `minimal` → `basic`★ → `standard` → `advanced` → `max`; one word instead of dozens of flags |
300
303
  | `swarmdo memory export/import -f obsidian` | **Obsidian vault roundtrip** — DB → markdown notes (YAML frontmatter, `[[wikilinks]]` stay live) → edit in Obsidian → sync back, re-embedded; `import --watch` keeps the vault live-synced as you edit |
301
304
  | `swarmdo memory backup` / `revectorize` | WAL-safe nightly DB snapshots · repair hash-era vectors |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "swarmdo",
3
- "version": "1.29.0",
3
+ "version": "1.37.0",
4
4
  "description": "Swarmdo - Enterprise AI agent orchestration for Claude Code. Deploy 60+ specialized agents in coordinated swarms with self-learning, fault-tolerant consensus, vector memory, and MCP integration",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -13,7 +13,7 @@
13
13
 
14
14
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
15
15
  import { join, dirname } from 'path';
16
- import { fileURLToPath } from 'url';
16
+ import { fileURLToPath, pathToFileURL } from 'url';
17
17
 
18
18
  const __filename = fileURLToPath(import.meta.url);
19
19
  const __dirname = dirname(__filename);
@@ -52,13 +52,30 @@ async function gracefulExit(signal) {
52
52
  process.on('SIGTERM', () => { gracefulExit('SIGTERM'); });
53
53
  process.on('SIGINT', () => { gracefulExit('SIGINT'); });
54
54
 
55
- // Ensure data dir
56
- if (!existsSync(DATA_DIR)) mkdirSync(DATA_DIR, { recursive: true });
57
-
58
55
  // ============================================================================
59
56
  // Simple JSON File Backend (implements IMemoryBackend interface)
60
57
  // ============================================================================
61
58
 
59
+ // Collapse entries that are the SAME memory — identical content signature but
60
+ // distinct ids — keeping the first occurrence. The auto-memory bridge
61
+ // historically re-imported every session with a fresh random id, bloating the
62
+ // store ~22x (#53); this heals already-bloated stores and de-dupes defensively.
63
+ // Keyed by namespace + content hash (or raw content) so genuinely distinct
64
+ // memories are never merged. Pure.
65
+ function dedupeByContentSignature(entries) {
66
+ const seen = new Set();
67
+ const out = [];
68
+ for (const e of entries) {
69
+ const ns = (e && e.namespace) || 'default';
70
+ const body = (e && e.metadata && e.metadata.contentHash) || (e && (e.content || e.value)) || '';
71
+ const sig = ns + '\u0001' + body;
72
+ if (seen.has(sig)) continue;
73
+ seen.add(sig);
74
+ out.push(e);
75
+ }
76
+ return out;
77
+ }
78
+
62
79
  class JsonFileBackend {
63
80
  constructor(filePath) {
64
81
  this.filePath = filePath;
@@ -70,7 +87,12 @@ class JsonFileBackend {
70
87
  try {
71
88
  const data = JSON.parse(readFileSync(this.filePath, 'utf-8'));
72
89
  if (Array.isArray(data)) {
73
- for (const entry of data) this.entries.set(entry.id, entry);
90
+ // Collapse content-duplicates left by the pre-#53 import bug (same
91
+ // memory re-imported each session under a fresh id). Auto-heals an
92
+ // already-bloated store by rewriting it once when dupes are dropped.
93
+ const deduped = dedupeByContentSignature(data);
94
+ for (const entry of deduped) this.entries.set(entry.id, entry);
95
+ if (deduped.length < data.length) this._persist();
74
96
  }
75
97
  } catch { /* start fresh */ }
76
98
  }
@@ -371,32 +393,43 @@ async function doStatus() {
371
393
  // Main
372
394
  // ============================================================================
373
395
 
374
- const command = process.argv[2] || 'status';
375
-
376
- // Dynamic import() failures can surface as unhandled rejections on a later
377
- // microtask even when the awaiting call site already caught them, which would
378
- // otherwise force a non-zero exit. Swallow to keep hooks exit-0, but surface the
379
- // reason under SWARMDO_DEBUG/DEBUG so genuine async bugs aren't silently hidden
380
- // (FIX 2 — the previous `() => {}` discarded every rejection process-wide).
381
- process.on('unhandledRejection', (reason) => {
382
- if (DEBUG) {
383
- const detail = reason && reason.message ? reason.message : String(reason);
384
- process.stderr.write(`[AutoMemory] unhandledRejection (suppressed): ${detail}\n`);
385
- }
386
- });
387
-
388
- try {
389
- switch (command) {
390
- case 'import': await doImport(); break;
391
- case 'sync': await doSync(); break;
392
- case 'status': await doStatus(); break;
393
- default:
394
- console.log('Usage: auto-memory-hook.mjs <import|sync|status>');
395
- break;
396
+ // Only dispatch CLI commands when this file is executed directly
397
+ // (`node auto-memory-hook.mjs …`), NOT when it is imported by a test —
398
+ // importing must not run a command or exit the process (#53 regression test
399
+ // imports JsonFileBackend + dedupeByContentSignature below).
400
+ if (import.meta.url === pathToFileURL(process.argv[1] || '').href) {
401
+ // Ensure data dir (only when actually running a command, not on import)
402
+ if (!existsSync(DATA_DIR)) mkdirSync(DATA_DIR, { recursive: true });
403
+ const command = process.argv[2] || 'status';
404
+
405
+ // Dynamic import() failures can surface as unhandled rejections on a later
406
+ // microtask even when the awaiting call site already caught them, which would
407
+ // otherwise force a non-zero exit. Swallow to keep hooks exit-0, but surface the
408
+ // reason under SWARMDO_DEBUG/DEBUG so genuine async bugs aren't silently hidden
409
+ // (FIX 2 — the previous `() => {}` discarded every rejection process-wide).
410
+ process.on('unhandledRejection', (reason) => {
411
+ if (DEBUG) {
412
+ const detail = reason && reason.message ? reason.message : String(reason);
413
+ process.stderr.write(`[AutoMemory] unhandledRejection (suppressed): ${detail}\n`);
414
+ }
415
+ });
416
+
417
+ try {
418
+ switch (command) {
419
+ case 'import': await doImport(); break;
420
+ case 'sync': await doSync(); break;
421
+ case 'status': await doStatus(); break;
422
+ default:
423
+ console.log('Usage: auto-memory-hook.mjs <import|sync|status>');
424
+ break;
425
+ }
426
+ } catch (err) {
427
+ // Hooks must never crash Claude Code - fail silently
428
+ try { dim(`Error (non-critical): ${err.message}`); } catch (_) {}
396
429
  }
397
- } catch (err) {
398
- // Hooks must never crash Claude Code - fail silently
399
- try { dim(`Error (non-critical): ${err.message}`); } catch (_) {}
430
+ // Force clean exit — process.exitCode alone isn't enough if async errors override it
431
+ process.exit(0);
400
432
  }
401
- // Force clean exit — process.exitCode alone isn't enough if async errors override it
402
- process.exit(0);
433
+
434
+ // Exported for unit tests (the run-guard above keeps import side-effect-free).
435
+ export { JsonFileBackend, dedupeByContentSignature };
@@ -10,7 +10,12 @@
10
10
  * diff and index load live in ../commands/affected.ts, so it's fixture-testable.
11
11
  */
12
12
  import type { CodeIndex } from '../codegraph/codegraph.js';
13
- /** Default matcher for test files (Jest/Vitest/Mocha conventions). */
13
+ /**
14
+ * Default matcher for test files (Jest/Vitest/Mocha conventions). A file under
15
+ * `__tests__/` counts only if it has a JS/TS extension — matching Jest's default
16
+ * `testMatch` (`**​/__tests__/**​/*.[jt]s?(x)`); docs/fixtures (`.md`, `.json`)
17
+ * living beside the tests are NOT test files and must not be fed to the runner.
18
+ */
14
19
  export declare function isTestFile(path: string): boolean;
15
20
  /** file it resolves to → files that import it (reverse of the import edges). Pure. */
16
21
  export declare function reverseDeps(index: CodeIndex): Map<string, string[]>;
@@ -9,9 +9,14 @@
9
9
  * closure). Pure — CodeIndex + changed paths in, affected/tests out; the git
10
10
  * diff and index load live in ../commands/affected.ts, so it's fixture-testable.
11
11
  */
12
- /** Default matcher for test files (Jest/Vitest/Mocha conventions). */
12
+ /**
13
+ * Default matcher for test files (Jest/Vitest/Mocha conventions). A file under
14
+ * `__tests__/` counts only if it has a JS/TS extension — matching Jest's default
15
+ * `testMatch` (`**​/__tests__/**​/*.[jt]s?(x)`); docs/fixtures (`.md`, `.json`)
16
+ * living beside the tests are NOT test files and must not be fed to the runner.
17
+ */
13
18
  export function isTestFile(path) {
14
- return /(^|\/)__tests__\//.test(path) || /\.(test|spec)\.[cm]?[jt]sx?$/.test(path);
19
+ return /(^|\/)__tests__\/.*\.[cm]?[jt]sx?$/.test(path) || /\.(test|spec)\.[cm]?[jt]sx?$/.test(path);
15
20
  }
16
21
  /** file it resolves to → files that import it (reverse of the import edges). Pure. */
17
22
  export function reverseDeps(index) {
@@ -184,16 +184,27 @@ export function applyPatch(source, patch, opts = {}) {
184
184
  // matched (trimmed) block against the current lines BEFORE splicing.
185
185
  const matchedBlock = oldBlock.slice(trimLead, oldBlock.length - trimTail);
186
186
  const ambiguous = countMatches(matchLines, matchedBlock) > 1;
187
- // Splice: remove oldBlock.length lines at `placed`, insert newBlock —
188
- // raw lines get the file's dominant EOL, the match twin stays stripped.
189
- const insertRaw = dominantCrlf ? newBlock.map((l) => l + '\r') : newBlock;
190
- lines.splice(placed, oldBlock.length, ...insertRaw);
191
- matchLines.splice(placed, oldBlock.length, ...newBlock);
192
- offset += newBlock.length - oldBlock.length;
187
+ // Splice ONLY the matched middle. When fuzz trimmed leading/trailing context
188
+ // (trimLead/trimTail > 0) the FULL block matched nowhere, so those trimmed
189
+ // context lines are unverified anchors the real source at those positions
190
+ // may differ (drift). Splicing the whole oldBlock.length at `placed` would
191
+ // delete a drifted-but-real line and rewrite it with the hunk's stale context
192
+ // copy — silent data loss on a line the hunk never marked to change. So splice
193
+ // from the matched position (placed + trimLead) over just the matched span,
194
+ // inserting newBlock minus the same lead/tail (context is identical on both
195
+ // sides), leaving the anchor lines exactly as the source has them. For a
196
+ // non-fuzzy hunk (trimLead = trimTail = 0) this is byte-identical to before.
197
+ const removeAt = placed + trimLead;
198
+ const removeCount = oldBlock.length - trimLead - trimTail;
199
+ const insert = newBlock.slice(trimLead, newBlock.length - trimTail);
200
+ const insertRaw = dominantCrlf ? insert.map((l) => l + '\r') : insert;
201
+ lines.splice(removeAt, removeCount, ...insertRaw);
202
+ matchLines.splice(removeAt, removeCount, ...insert);
203
+ offset += insert.length - removeCount; // === newBlock.length - oldBlock.length
193
204
  results.push({ hunk, applied: true, at: placed, fuzzUsed: usedFuzz, ...(ambiguous && { ambiguous: true }) });
194
205
  // If this hunk's new content is now the tail of the file, its markers
195
206
  // determine whether the file ends with a newline.
196
- if (placed + newBlock.length === lines.length)
207
+ if (removeAt + insert.length === lines.length)
197
208
  eofNoEol = newSideEndsNoEol(hunk);
198
209
  }
199
210
  let result = lines.join('\n');
@@ -44,7 +44,10 @@ export function parseCommit(hash, subject, body = '') {
44
44
  hash,
45
45
  type: 'revert',
46
46
  scope: inner ? inner[2] ?? null : null,
47
- breaking: BREAKING_RE.test(body),
47
+ // A revert of a breaking commit is itself breaking — mirror the non-revert
48
+ // branch and consult the reverted subject's `!` marker (inner group 3),
49
+ // not just a BREAKING CHANGE footer on the revert body.
50
+ breaking: (inner ? inner[3] === '!' : false) || BREAKING_RE.test(body),
48
51
  subject: inner ? inner[4].trim() : rev[1].trim(),
49
52
  raw: trimmed,
50
53
  };
@@ -37,6 +37,7 @@ const MATCHERS = [
37
37
  function declaratorNames(decl) {
38
38
  const names = [];
39
39
  let depth = 0;
40
+ let angle = 0; // generic type-argument depth: `Map<string, number>`
40
41
  let start = 0;
41
42
  let str = null;
42
43
  const take = (seg) => {
@@ -57,7 +58,16 @@ function declaratorNames(decl) {
57
58
  depth++;
58
59
  else if (ch === ')' || ch === ']' || ch === '}')
59
60
  depth--;
60
- else if (ch === ',' && depth === 0) {
61
+ // Generic type-args: a comma inside `Map<string, number>` must not split the
62
+ // declarator list. Only treat `<` as an opener when it directly follows an
63
+ // identifier char (Map<, Record<, Array<) — so a spaced comparison `a < b`
64
+ // isn't misread as a generic — and exclude `<<`/`<=`. Close on `>`, skipping
65
+ // the `>` of an arrow `=>` so `Record<string, () => void>` stays balanced.
66
+ else if (ch === '<' && i > 0 && /[\w$]/.test(decl[i - 1]) && decl[i - 1] !== '<' && decl[i + 1] !== '<' && decl[i + 1] !== '=')
67
+ angle++;
68
+ else if (ch === '>' && angle > 0 && decl[i - 1] !== '=')
69
+ angle--;
70
+ else if (ch === ',' && depth === 0 && angle === 0) {
61
71
  take(decl.slice(start, i));
62
72
  start = i + 1;
63
73
  }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * comms.ts — `swarmdo comms` — a cross-session agent mailbox (#44).
3
+ *
4
+ * swarmdo comms send -t <session> -m "<message>" leave a message for a session
5
+ * swarmdo comms inbox [-t <self>] [-u] list your messages (newest first)
6
+ * swarmdo comms read <id> show a full message + mark it read
7
+ *
8
+ * Any Claude Code session on the same repo shares `.swarmdo/comms/store.json`, so
9
+ * one session can message another by name ("multiplayer swarms"). Address `all`
10
+ * to broadcast. The message model, filtering, and retention live in the pure
11
+ * comms/mailbox.ts engine; this layer is fs persistence + presentation.
12
+ */
13
+ import type { Command } from '../types.js';
14
+ export declare const commsCommand: Command;
15
+ export default commsCommand;
16
+ //# sourceMappingURL=comms.d.ts.map