nexus-agents 2.77.12 → 2.78.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 (131) hide show
  1. package/dist/{child-mcp-config-MJMUF7TL.js → child-mcp-config-CTO2MBRM.js} +3 -4
  2. package/dist/{child-mcp-config-MJMUF7TL.js.map → child-mcp-config-CTO2MBRM.js.map} +1 -1
  3. package/dist/{chunk-YJ2IGAD2.js → chunk-2UYTFLMO.js} +2 -2
  4. package/dist/{chunk-6AY5DK4E.js → chunk-2YPG6PDG.js} +3 -3
  5. package/dist/{chunk-3VWMM6UF.js → chunk-3NIPH6UP.js} +2 -2
  6. package/dist/{chunk-L3TPDTP3.js → chunk-4N33QZLH.js} +13 -15
  7. package/dist/{chunk-L3TPDTP3.js.map → chunk-4N33QZLH.js.map} +1 -1
  8. package/dist/{chunk-JN6UWGHH.js → chunk-5O6XLBPP.js} +2 -2
  9. package/dist/{chunk-ERWXGXV2.js → chunk-6TFTVW77.js} +3 -3
  10. package/dist/{chunk-2IAWMNNB.js → chunk-6WBTNZAY.js} +183 -87
  11. package/dist/chunk-6WBTNZAY.js.map +1 -0
  12. package/dist/{chunk-GOT7OAL5.js → chunk-7BMOZJYS.js} +29 -5
  13. package/dist/chunk-7BMOZJYS.js.map +1 -0
  14. package/dist/{chunk-C2LLQ6TW.js → chunk-7XCUZI4G.js} +4 -4
  15. package/dist/chunk-7XCUZI4G.js.map +1 -0
  16. package/dist/{chunk-TDV5ALHY.js → chunk-D6TM2VHX.js} +3 -3
  17. package/dist/{chunk-PWTJGGKB.js → chunk-DLXT23AC.js} +2 -2
  18. package/dist/chunk-DNO2INX5.js +276 -0
  19. package/dist/chunk-DNO2INX5.js.map +1 -0
  20. package/dist/{chunk-G2CSKBY5.js → chunk-FJWWSVWB.js} +29 -6
  21. package/dist/chunk-FJWWSVWB.js.map +1 -0
  22. package/dist/{chunk-DSQ5XM4O.js → chunk-FVPYP5DD.js} +4 -4
  23. package/dist/{chunk-MGLWPN2I.js → chunk-GONMG4NM.js} +2 -2
  24. package/dist/{chunk-XYA3DPWJ.js → chunk-GTGDVBLW.js} +5 -5
  25. package/dist/{chunk-YQMQSJQK.js → chunk-HYU4GZY6.js} +2 -2
  26. package/dist/{chunk-3DH5SLFH.js → chunk-K2QILJG4.js} +6 -6
  27. package/dist/{chunk-5WHWKY32.js → chunk-KT5FIBWS.js} +2 -2
  28. package/dist/{chunk-DIB6V67T.js → chunk-L6SCKLGO.js} +3 -3
  29. package/dist/{chunk-IPWCD22D.js → chunk-PLX6FCFC.js} +2 -2
  30. package/dist/chunk-PQHVC4BD.js +639 -0
  31. package/dist/chunk-PQHVC4BD.js.map +1 -0
  32. package/dist/chunk-Q5CFPIJ5.js +5581 -0
  33. package/dist/chunk-Q5CFPIJ5.js.map +1 -0
  34. package/dist/{chunk-G6ZPVADX.js → chunk-SD76JZBG.js} +2 -2
  35. package/dist/{chunk-Y2CP4Z5B.js → chunk-SWFJU3W2.js} +220 -4580
  36. package/dist/chunk-SWFJU3W2.js.map +1 -0
  37. package/dist/{chunk-3MRM53T4.js → chunk-WDYCIJWN.js} +640 -470
  38. package/dist/chunk-WDYCIJWN.js.map +1 -0
  39. package/dist/{chunk-CM3TORGV.js → chunk-YXWGEIQR.js} +2 -2
  40. package/dist/{chunk-7NK7BTWP.js → chunk-ZVCED4Z4.js} +2 -2
  41. package/dist/cli-circuit-breaker-I74ZQ44Q.js +13 -0
  42. package/dist/cli.js +109 -58
  43. package/dist/cli.js.map +1 -1
  44. package/dist/{composite-router-S6E26BCI.js → composite-router-V3OC57IE.js} +3 -4
  45. package/dist/consensus-vote-ESFPGEJE.js +30 -0
  46. package/dist/context-retriever-MB3D7KS6.js +18 -0
  47. package/dist/dist-NIXVXYIH.js +42 -0
  48. package/dist/doctor-deep-KQ765XZA.js +12 -0
  49. package/dist/expert-bridge-JKLC57IC.js +10 -0
  50. package/dist/factory-BUUXNGIB.js +14 -0
  51. package/dist/{factory-X3VKIGKP.js → factory-LHHYDVZX.js} +5 -6
  52. package/dist/index.d.ts +72 -8
  53. package/dist/index.js +208 -316
  54. package/dist/index.js.map +1 -1
  55. package/dist/{init-opencode-CFE7M6XA.js → init-opencode-GXZN2W5S.js} +6 -7
  56. package/dist/{init-opencode-CFE7M6XA.js.map → init-opencode-GXZN2W5S.js.map} +1 -1
  57. package/dist/issue-triage-RMXPDZ2K.js +15 -0
  58. package/dist/{learning-persistence-N6ILD2HX.js → learning-persistence-Q3HTOGTU.js} +2 -3
  59. package/dist/outcome-store-adapter-QRFJJIKB.js +57 -0
  60. package/dist/outcome-store-adapter-QRFJJIKB.js.map +1 -0
  61. package/dist/{registry-command-RPPC7N2K.js → registry-command-6E4YKAMT.js} +3 -4
  62. package/dist/{registry-command-RPPC7N2K.js.map → registry-command-6E4YKAMT.js.map} +1 -1
  63. package/dist/{repo-security-plan-7ZCDVH5O.js → repo-security-plan-AGRU72DL.js} +4 -5
  64. package/dist/research-helpers-synthesize-K2UCJQQG.js +13 -0
  65. package/dist/{routing-memory-5VTX7LQX.js → routing-memory-3B6DDZ76.js} +3 -4
  66. package/dist/{session-memory-7XBV6BMY.js → session-memory-L7EQIY2O.js} +4 -5
  67. package/dist/{setup-command-3ZTEPKDA.js → setup-command-VYV4RFWW.js} +11 -12
  68. package/dist/setup-config-EQT24DD4.js +10 -0
  69. package/dist/{setup-custom-api-WM5W5AY5.js → setup-custom-api-IBDV654K.js} +5 -6
  70. package/dist/{setup-custom-api-WM5W5AY5.js.map → setup-custom-api-IBDV654K.js.map} +1 -1
  71. package/dist/tool-memory-6HCHQLAN.js +19 -0
  72. package/dist/{weather-report-YJMVKJGA.js → weather-report-ER3WUZ7S.js} +3 -4
  73. package/package.json +3 -2
  74. package/dist/adaptive-memory-EI564K4C.js +0 -16
  75. package/dist/chunk-2IAWMNNB.js.map +0 -1
  76. package/dist/chunk-3MRM53T4.js.map +0 -1
  77. package/dist/chunk-BJ2OMC7P.js +0 -944
  78. package/dist/chunk-BJ2OMC7P.js.map +0 -1
  79. package/dist/chunk-C2LLQ6TW.js.map +0 -1
  80. package/dist/chunk-G2CSKBY5.js.map +0 -1
  81. package/dist/chunk-GOT7OAL5.js.map +0 -1
  82. package/dist/chunk-I7ORMAO7.js +0 -32
  83. package/dist/chunk-I7ORMAO7.js.map +0 -1
  84. package/dist/chunk-Y2CP4Z5B.js.map +0 -1
  85. package/dist/cli-circuit-breaker-YX4BWZD5.js +0 -14
  86. package/dist/consensus-vote-MUQ4HPIF.js +0 -30
  87. package/dist/doctor-deep-BRU5ZUJI.js +0 -13
  88. package/dist/expert-bridge-ZPNVLJVN.js +0 -11
  89. package/dist/factory-A7DTCCUY.js +0 -15
  90. package/dist/issue-triage-6XD6CVPB.js +0 -16
  91. package/dist/mobimem-CG2MNS7V.js +0 -14
  92. package/dist/nexus-data-dir-77UO7N6J.js +0 -12
  93. package/dist/research-helpers-synthesize-36TUTUUA.js +0 -14
  94. package/dist/setup-config-EI5KROA3.js +0 -11
  95. /package/dist/{chunk-YJ2IGAD2.js.map → chunk-2UYTFLMO.js.map} +0 -0
  96. /package/dist/{chunk-6AY5DK4E.js.map → chunk-2YPG6PDG.js.map} +0 -0
  97. /package/dist/{chunk-3VWMM6UF.js.map → chunk-3NIPH6UP.js.map} +0 -0
  98. /package/dist/{chunk-JN6UWGHH.js.map → chunk-5O6XLBPP.js.map} +0 -0
  99. /package/dist/{chunk-ERWXGXV2.js.map → chunk-6TFTVW77.js.map} +0 -0
  100. /package/dist/{chunk-TDV5ALHY.js.map → chunk-D6TM2VHX.js.map} +0 -0
  101. /package/dist/{chunk-PWTJGGKB.js.map → chunk-DLXT23AC.js.map} +0 -0
  102. /package/dist/{chunk-DSQ5XM4O.js.map → chunk-FVPYP5DD.js.map} +0 -0
  103. /package/dist/{chunk-MGLWPN2I.js.map → chunk-GONMG4NM.js.map} +0 -0
  104. /package/dist/{chunk-XYA3DPWJ.js.map → chunk-GTGDVBLW.js.map} +0 -0
  105. /package/dist/{chunk-YQMQSJQK.js.map → chunk-HYU4GZY6.js.map} +0 -0
  106. /package/dist/{chunk-3DH5SLFH.js.map → chunk-K2QILJG4.js.map} +0 -0
  107. /package/dist/{chunk-5WHWKY32.js.map → chunk-KT5FIBWS.js.map} +0 -0
  108. /package/dist/{chunk-DIB6V67T.js.map → chunk-L6SCKLGO.js.map} +0 -0
  109. /package/dist/{chunk-IPWCD22D.js.map → chunk-PLX6FCFC.js.map} +0 -0
  110. /package/dist/{chunk-G6ZPVADX.js.map → chunk-SD76JZBG.js.map} +0 -0
  111. /package/dist/{chunk-CM3TORGV.js.map → chunk-YXWGEIQR.js.map} +0 -0
  112. /package/dist/{chunk-7NK7BTWP.js.map → chunk-ZVCED4Z4.js.map} +0 -0
  113. /package/dist/{adaptive-memory-EI564K4C.js.map → cli-circuit-breaker-I74ZQ44Q.js.map} +0 -0
  114. /package/dist/{cli-circuit-breaker-YX4BWZD5.js.map → composite-router-V3OC57IE.js.map} +0 -0
  115. /package/dist/{composite-router-S6E26BCI.js.map → consensus-vote-ESFPGEJE.js.map} +0 -0
  116. /package/dist/{consensus-vote-MUQ4HPIF.js.map → context-retriever-MB3D7KS6.js.map} +0 -0
  117. /package/dist/{doctor-deep-BRU5ZUJI.js.map → dist-NIXVXYIH.js.map} +0 -0
  118. /package/dist/{expert-bridge-ZPNVLJVN.js.map → doctor-deep-KQ765XZA.js.map} +0 -0
  119. /package/dist/{factory-A7DTCCUY.js.map → expert-bridge-JKLC57IC.js.map} +0 -0
  120. /package/dist/{factory-X3VKIGKP.js.map → factory-BUUXNGIB.js.map} +0 -0
  121. /package/dist/{issue-triage-6XD6CVPB.js.map → factory-LHHYDVZX.js.map} +0 -0
  122. /package/dist/{learning-persistence-N6ILD2HX.js.map → issue-triage-RMXPDZ2K.js.map} +0 -0
  123. /package/dist/{mobimem-CG2MNS7V.js.map → learning-persistence-Q3HTOGTU.js.map} +0 -0
  124. /package/dist/{nexus-data-dir-77UO7N6J.js.map → repo-security-plan-AGRU72DL.js.map} +0 -0
  125. /package/dist/{repo-security-plan-7ZCDVH5O.js.map → research-helpers-synthesize-K2UCJQQG.js.map} +0 -0
  126. /package/dist/{research-helpers-synthesize-36TUTUUA.js.map → routing-memory-3B6DDZ76.js.map} +0 -0
  127. /package/dist/{routing-memory-5VTX7LQX.js.map → session-memory-L7EQIY2O.js.map} +0 -0
  128. /package/dist/{session-memory-7XBV6BMY.js.map → setup-command-VYV4RFWW.js.map} +0 -0
  129. /package/dist/{setup-command-3ZTEPKDA.js.map → setup-config-EQT24DD4.js.map} +0 -0
  130. /package/dist/{setup-config-EI5KROA3.js.map → tool-memory-6HCHQLAN.js.map} +0 -0
  131. /package/dist/{weather-report-YJMVKJGA.js.map → weather-report-ER3WUZ7S.js.map} +0 -0
@@ -1,3 +1,7 @@
1
+ // src/config/learning-persistence.ts
2
+ import { mkdirSync } from "fs";
3
+ import { join as join2 } from "path";
4
+
1
5
  // src/config/nexus-data-dir.ts
2
6
  import { homedir } from "os";
3
7
  import { join, resolve } from "path";
@@ -44,16 +48,36 @@ function getNexusDataDir() {
44
48
  }
45
49
  return join(homedir(), ".nexus-agents");
46
50
  }
47
- function resetNexusDataDirCache() {
48
- }
49
51
  function nexusDataPath(...segments) {
50
52
  return join(getNexusDataDir(), ...segments);
51
53
  }
52
54
 
55
+ // src/config/learning-persistence.ts
56
+ function getLearningDir() {
57
+ return join2(getNexusDataDir(), "learning");
58
+ }
59
+ function getOutcomesFile() {
60
+ return join2(getLearningDir(), "outcomes.jsonl");
61
+ }
62
+ function getRulesFile() {
63
+ return join2(getLearningDir(), "rules.json");
64
+ }
65
+ var DIR_MODE = 448;
66
+ function isPersistenceEnabled() {
67
+ return process.env["NEXUS_PERSIST_LEARNING"] !== "false";
68
+ }
69
+ function ensureLearningDir(dir) {
70
+ mkdirSync(dir ?? getLearningDir(), { recursive: true, mode: DIR_MODE });
71
+ }
72
+
53
73
  export {
54
74
  detectSandbox,
55
75
  getNexusDataDir,
56
- resetNexusDataDirCache,
57
- nexusDataPath
76
+ nexusDataPath,
77
+ getLearningDir,
78
+ getOutcomesFile,
79
+ getRulesFile,
80
+ isPersistenceEnabled,
81
+ ensureLearningDir
58
82
  };
59
- //# sourceMappingURL=chunk-GOT7OAL5.js.map
83
+ //# sourceMappingURL=chunk-7BMOZJYS.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/config/learning-persistence.ts","../src/config/nexus-data-dir.ts","../src/config/sandbox-detection.ts"],"sourcesContent":["/**\n * Shared configuration for cross-session learning persistence.\n *\n * Controls where learning data (outcomes, distilled rules) is stored\n * on disk and whether persistence is enabled via feature flag.\n *\n * @module config/learning-persistence\n * (Source: Issue #1009 — Cross-session persistence)\n */\n\nimport { mkdirSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { getNexusDataDir } from './nexus-data-dir.js';\n\n// ============================================================================\n// Path resolution (#2316: must read NEXUS_DATA_DIR at call time, not import)\n// ============================================================================\n\n/**\n * Base directory for learning persistence data. Resolved at call time so\n * `NEXUS_DATA_DIR` overrides take effect — the const-at-import-time\n * pattern this replaces was the bug discovered while dogfooding v2.63.0\n * (#2316). Outcome counts on a fresh portable workspace were leaking\n * the host home directory's outcome history.\n */\nexport function getLearningDir(): string {\n return join(getNexusDataDir(), 'learning');\n}\n\n/** JSONL file for persisted task outcomes. */\nexport function getOutcomesFile(): string {\n return join(getLearningDir(), 'outcomes.jsonl');\n}\n\n/** JSON file for persisted distilled rules. */\nexport function getRulesFile(): string {\n return join(getLearningDir(), 'rules.json');\n}\n\n// Note: previous LEARNING_DIR / OUTCOMES_FILE / RULES_FILE exports were\n// removed in #2316 — they were evaluated at module import time and ignored\n// `NEXUS_DATA_DIR`. All callers must use the getter functions above.\n\n/** Directory mode: owner-only (rwx------). */\nconst DIR_MODE = 0o700;\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\n/**\n * Check whether learning persistence is enabled via feature flag.\n *\n * Defaults to true — cross-session LinUCB routing data persists to\n * `~/.nexus-agents/learning/` unless explicitly disabled.\n * Only routing metadata is stored (model, success, duration, category).\n * No user prompts, API keys, or model outputs are persisted.\n *\n * Set NEXUS_PERSIST_LEARNING=false to disable.\n */\nexport function isPersistenceEnabled(): boolean {\n return process.env['NEXUS_PERSIST_LEARNING'] !== 'false';\n}\n\n/** Ensure the learning data directory exists. */\nexport function ensureLearningDir(dir?: string): void {\n mkdirSync(dir ?? getLearningDir(), { recursive: true, mode: DIR_MODE });\n}\n","/**\n * Nexus runtime data directory resolver (#2302, child of #2301).\n *\n * Returns the absolute path under which nexus-agents stores all runtime\n * state — memory, learning, audit, voting, sessions, checkpoints, traces,\n * model registry. Single source of truth so portable / sandbox / CI\n * deployments can redirect state to a workspace-local folder via the\n * `NEXUS_DATA_DIR` environment variable.\n *\n * Resolution order (first match wins):\n * 1. `NEXUS_DATA_DIR` env var if set + non-empty (resolved against `cwd`).\n * 2. Sandbox-mode default (#2501): when `NEXUS_SANDBOX` is set, use\n * `${NEXUS_SANDBOX_ROOT ?? '/'}/.nexus-agents`. Sandboxed deployments\n * typically mount a multi-repo root; state goes there, shared across\n * repo subfolders rather than buried inside one.\n * 3. `<homedir>/.nexus-agents` (zero-breakage fallback for laptop use).\n *\n * No caching, no filesystem walks, no discovery. The contrarian-narrowed\n * scope (#2301 vote) explicitly defers ancestor-walking to a separate\n * child with a security design pass per CVE-2022-24765. Recomputing the\n * trivial env-or-homedir lookup on each call is ~100ns and avoids cache\n * coordination issues with tests that mock `homedir()`.\n *\n * @module config/nexus-data-dir\n */\n\nimport { homedir } from 'node:os';\nimport { join, resolve } from 'node:path';\n\nimport { detectSandbox } from './sandbox-detection.js';\n\n/** Returns the absolute path to the nexus-agents data directory. */\nexport function getNexusDataDir(): string {\n const fromEnv = process.env['NEXUS_DATA_DIR']?.trim();\n if (fromEnv !== undefined && fromEnv !== '') {\n return resolve(fromEnv);\n }\n const sandbox = detectSandbox();\n if (sandbox.active) {\n return resolve(sandbox.root ?? '/', '.nexus-agents');\n }\n return join(homedir(), '.nexus-agents');\n}\n\n/**\n * No-op kept for source-compatibility with consumers that called this\n * earlier in development. The resolver is no longer cached, so resetting\n * is unnecessary. Kept exported (rather than removed) to avoid breaking\n * imports in tests that may have already adopted it.\n */\nexport function resetNexusDataDirCache(): void {\n // intentionally empty — see module docstring\n}\n\n/** Returns a path joined under the resolved data directory. */\nexport function nexusDataPath(...segments: string[]): string {\n return join(getNexusDataDir(), ...segments);\n}\n","/**\n * Sandbox detection (#2501, child 1 of epic #2500).\n *\n * nexus-agents needs to know whether it's running inside a host-provided\n * sandbox (Docker Desktop Sandbox + OpenCode, Codex sandbox, locked-down\n * CI runner) so it can adjust behaviour:\n *\n * - default `NEXUS_DATA_DIR` to the multi-repo root rather than `~/...`\n * - skip CLI subprocess detection (binaries aren't there)\n * - fail-fast when the gateway is unreachable instead of degrading\n * - suppress diagnostics that don't apply\n *\n * The signal is **explicit**: the image author (Dockerfile, devcontainer,\n * harness wrapper) sets `NEXUS_SANDBOX=<flavor>`. The flavor string is\n * for diagnostics + per-flavor branching (`docker-opencode`, `codex`,\n * `claude-code`, `ci-restricted`, …); presence is the on/off signal.\n *\n * A heuristic check runs alongside for verification — if `NEXUS_SANDBOX`\n * claims `docker-opencode` but `/.dockerenv` is missing, that's a\n * misconfiguration worth surfacing to the operator.\n *\n * @module config/sandbox-detection\n */\n\nimport { existsSync, readFileSync } from 'node:fs';\n\n/**\n * Heuristic detection result. Independent from the explicit\n * `NEXUS_SANDBOX` env var; produced by inspecting filesystem markers.\n */\nexport type SandboxHeuristic = 'docker' | 'podman' | 'unknown' | null;\n\nexport interface SandboxInfo {\n /**\n * True iff `NEXUS_SANDBOX` is set + non-empty. The explicit signal\n * the rest of the codebase keys off — never `true` by heuristic alone.\n */\n readonly active: boolean;\n /**\n * Operator-supplied flavor string, e.g. `docker-opencode`. Undefined\n * when `active === false` or the env var is empty.\n */\n readonly flavor: string | undefined;\n /**\n * Multi-repo root the user mounted. From `NEXUS_SANDBOX_ROOT`. Undefined\n * when unset; consumers that need a default substitute `/`.\n */\n readonly root: string | undefined;\n /**\n * Independent heuristic match — `null` when we couldn't run the check,\n * `'unknown'` when ran but no marker matched. Used by `doctor` to flag\n * mismatches between the explicit signal and the runtime environment.\n */\n readonly heuristicMatch: SandboxHeuristic;\n}\n\n/**\n * Detect whether nexus-agents is running inside a host-provided sandbox.\n * Pure read of env + filesystem; no caching (cheap enough to recompute,\n * and tests routinely mutate process.env).\n */\nexport function detectSandbox(): SandboxInfo {\n const flavorRaw = process.env['NEXUS_SANDBOX']?.trim();\n const flavor = flavorRaw !== undefined && flavorRaw !== '' ? flavorRaw : undefined;\n const active = flavor !== undefined;\n\n const rootRaw = process.env['NEXUS_SANDBOX_ROOT']?.trim();\n const root = rootRaw !== undefined && rootRaw !== '' ? rootRaw : undefined;\n\n return {\n active,\n flavor,\n root,\n heuristicMatch: detectContainerHeuristic(),\n };\n}\n\n/**\n * Look for filesystem markers indicating a container runtime. Used as a\n * cross-check against the explicit `NEXUS_SANDBOX` signal.\n *\n * Order of checks:\n * 1. `/.dockerenv` (Docker)\n * 2. `/run/.containerenv` (Podman)\n * 3. `/proc/1/cgroup` containing `docker` or `containerd` strings\n *\n * Returns `null` when none of the checks could run (non-Linux host with\n * no `/proc`, sandbox blocks `existsSync`, etc.) — distinct from\n * `'unknown'` (checks ran, no marker matched) so the doctor message can\n * differentiate \"we couldn't tell\" from \"we checked, no container\".\n */\nfunction detectContainerHeuristic(): SandboxHeuristic {\n try {\n if (existsSync('/.dockerenv')) return 'docker';\n if (existsSync('/run/.containerenv')) return 'podman';\n if (existsSync('/proc/1/cgroup')) {\n const cgroup = readFileSync('/proc/1/cgroup', 'utf8');\n if (/\\bdocker\\b/.test(cgroup)) return 'docker';\n if (/\\bcontainerd\\b/.test(cgroup)) return 'docker';\n }\n return 'unknown';\n } catch {\n return null;\n }\n}\n"],"mappings":";AAUA,SAAS,iBAAiB;AAC1B,SAAS,QAAAA,aAAY;;;ACerB,SAAS,eAAe;AACxB,SAAS,MAAM,eAAe;;;ACH9B,SAAS,YAAY,oBAAoB;AAqClC,SAAS,gBAA6B;AAC3C,QAAM,YAAY,QAAQ,IAAI,eAAe,GAAG,KAAK;AACrD,QAAM,SAAS,cAAc,UAAa,cAAc,KAAK,YAAY;AACzE,QAAM,SAAS,WAAW;AAE1B,QAAM,UAAU,QAAQ,IAAI,oBAAoB,GAAG,KAAK;AACxD,QAAM,OAAO,YAAY,UAAa,YAAY,KAAK,UAAU;AAEjE,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA,gBAAgB,yBAAyB;AAAA,EAC3C;AACF;AAgBA,SAAS,2BAA6C;AACpD,MAAI;AACF,QAAI,WAAW,aAAa,EAAG,QAAO;AACtC,QAAI,WAAW,oBAAoB,EAAG,QAAO;AAC7C,QAAI,WAAW,gBAAgB,GAAG;AAChC,YAAM,SAAS,aAAa,kBAAkB,MAAM;AACpD,UAAI,aAAa,KAAK,MAAM,EAAG,QAAO;AACtC,UAAI,iBAAiB,KAAK,MAAM,EAAG,QAAO;AAAA,IAC5C;AACA,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;;;ADxEO,SAAS,kBAA0B;AACxC,QAAM,UAAU,QAAQ,IAAI,gBAAgB,GAAG,KAAK;AACpD,MAAI,YAAY,UAAa,YAAY,IAAI;AAC3C,WAAO,QAAQ,OAAO;AAAA,EACxB;AACA,QAAM,UAAU,cAAc;AAC9B,MAAI,QAAQ,QAAQ;AAClB,WAAO,QAAQ,QAAQ,QAAQ,KAAK,eAAe;AAAA,EACrD;AACA,SAAO,KAAK,QAAQ,GAAG,eAAe;AACxC;AAaO,SAAS,iBAAiB,UAA4B;AAC3D,SAAO,KAAK,gBAAgB,GAAG,GAAG,QAAQ;AAC5C;;;ADhCO,SAAS,iBAAyB;AACvC,SAAOC,MAAK,gBAAgB,GAAG,UAAU;AAC3C;AAGO,SAAS,kBAA0B;AACxC,SAAOA,MAAK,eAAe,GAAG,gBAAgB;AAChD;AAGO,SAAS,eAAuB;AACrC,SAAOA,MAAK,eAAe,GAAG,YAAY;AAC5C;AAOA,IAAM,WAAW;AAgBV,SAAS,uBAAgC;AAC9C,SAAO,QAAQ,IAAI,wBAAwB,MAAM;AACnD;AAGO,SAAS,kBAAkB,KAAoB;AACpD,YAAU,OAAO,eAAe,GAAG,EAAE,WAAW,MAAM,MAAM,SAAS,CAAC;AACxE;","names":["join","join"]}
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  OpenAIAdapter
3
- } from "./chunk-DIB6V67T.js";
3
+ } from "./chunk-L6SCKLGO.js";
4
4
  import {
5
5
  ConfigError,
6
6
  createLogger,
@@ -9,10 +9,10 @@ import {
9
9
  getTimeProvider,
10
10
  lookupInTreeCapability,
11
11
  ok
12
- } from "./chunk-3MRM53T4.js";
12
+ } from "./chunk-WDYCIJWN.js";
13
13
  import {
14
14
  getNexusDataDir
15
- } from "./chunk-GOT7OAL5.js";
15
+ } from "./chunk-7BMOZJYS.js";
16
16
 
17
17
  // src/config/opencode-bridge.ts
18
18
  import { readFileSync } from "fs";
@@ -307,4 +307,4 @@ export {
307
307
  discoverModels,
308
308
  buildOpenAICompatAdapters
309
309
  };
310
- //# sourceMappingURL=chunk-C2LLQ6TW.js.map
310
+ //# sourceMappingURL=chunk-7XCUZI4G.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/config/opencode-bridge.ts","../src/adapters/openai-compat-adapter.ts","../src/learning/usage-log.ts"],"sourcesContent":["/**\n * opencode.json gateway-config bridge (#2503, child 3 of epic #2500).\n *\n * When nexus-agents runs as an MCP loaded by OpenCode (typically inside a\n * Docker sandbox), OpenCode's `opencode.json` already declares the\n * OpenAI-compatible gateway the harness uses. Reading it from there saves\n * the operator from re-declaring the same `baseURL` + `apiKey` as\n * NEXUS_OPENAI_COMPAT_* env vars.\n *\n * Scope (per direction discussion 2026-05-09):\n * - **Only** reads `providers.openai-compat.options.{baseURL, apiKey}`.\n * Does NOT read OpenCode's MCP config, model list, logging, or other\n * settings. nexus-agents' own behaviour stays driven by\n * `nexus-agents.yaml` + `NEXUS_*` env vars.\n * - Resolves OpenCode's `{env:VAR}` interpolation in the apiKey field.\n * - Returns `null` on any failure path (missing file, parse error,\n * missing fields, unset interpolated env var) — caller falls back to\n * env-var precedence; we don't throw.\n *\n * Precedence (applied in `openai-compat-adapter.ts`):\n * 1. `NEXUS_OPENAI_COMPAT_URL` + `NEXUS_OPENAI_COMPAT_KEY` env vars (if both set)\n * 2. `NEXUS_OPENCODE_CONFIG` → `opencode.json` → `providers.openai-compat`\n * 3. Unconfigured → `null`, no adapter registered\n *\n * Sanitised logging: never log the resolved apiKey. Only the baseURL is\n * logged on success.\n *\n * @module config/opencode-bridge\n */\n\nimport { readFileSync } from 'node:fs';\n\nimport { createLogger } from '../core/index.js';\n\nconst logger = createLogger({ component: 'opencode-bridge' });\n\nexport interface OpencodeGatewayConfig {\n readonly baseURL: string;\n readonly apiKey: string;\n}\n\n/**\n * Read `providers.openai-compat.options.{baseURL, apiKey}` from the given\n * opencode.json path. Returns `null` on any failure — the caller falls\n * back to env-var precedence.\n */\nexport function readOpencodeGateway(path: string): OpencodeGatewayConfig | null {\n const raw = readFileOrNull(path);\n if (raw === null) return null;\n\n const parsed = parseJsonOrNull(raw, path);\n if (parsed === null) return null;\n\n const options = extractOpenAICompatOptions(parsed);\n if (options === null) {\n logger.debug('opencode.json has no providers.openai-compat.options block', { path });\n return null;\n }\n\n const baseURL = typeof options.baseURL === 'string' ? options.baseURL.trim() : '';\n const apiKeyRaw = typeof options.apiKey === 'string' ? options.apiKey.trim() : '';\n if (baseURL === '' || apiKeyRaw === '') {\n logger.warn('opencode.json providers.openai-compat missing baseURL or apiKey', { path });\n return null;\n }\n\n const apiKey = resolveEnvInterpolation(apiKeyRaw);\n if (apiKey === null) {\n logger.warn(\n 'opencode.json apiKey references an env var that is not set; gateway not configured',\n { path }\n );\n return null;\n }\n\n logger.info('Gateway config sourced from opencode.json', { baseURL, path });\n return { baseURL, apiKey };\n}\n\nfunction readFileOrNull(path: string): string | null {\n try {\n return readFileSync(path, 'utf8');\n } catch (err: unknown) {\n const msg = err instanceof Error ? err.message : String(err);\n logger.debug('Could not read opencode.json', { path, error: msg });\n return null;\n }\n}\n\nfunction parseJsonOrNull(raw: string, path: string): unknown {\n try {\n return JSON.parse(raw);\n } catch (err: unknown) {\n const msg = err instanceof Error ? err.message : String(err);\n logger.warn('opencode.json is not valid JSON; ignoring', { path, error: msg });\n return null;\n }\n}\n\n/**\n * Pull `providers.openai-compat.options` out of the parsed JSON without\n * coupling to a Zod schema. OpenCode's config has many keys we don't\n * care about; we only navigate to the one we need.\n */\nfunction extractOpenAICompatOptions(\n parsed: unknown\n): { baseURL?: unknown; apiKey?: unknown } | null {\n if (typeof parsed !== 'object' || parsed === null) return null;\n const root = parsed as { providers?: unknown };\n const providers = root.providers;\n if (typeof providers !== 'object' || providers === null) return null;\n const provider = (providers as Record<string, unknown>)['openai-compat'];\n if (typeof provider !== 'object' || provider === null) return null;\n const options = (provider as { options?: unknown }).options;\n if (typeof options !== 'object' || options === null) return null;\n return options;\n}\n\n/**\n * Resolve OpenCode's `{env:VAR}` interpolation. Only the literal pattern\n * is supported (no shell expansion, no defaults). Returns the string as-is\n * when no interpolation is present; returns `null` when the referenced\n * env var is not set.\n */\nfunction resolveEnvInterpolation(value: string): string | null {\n const match = /^\\{env:([A-Z0-9_]+)\\}$/.exec(value);\n if (match === null) return value;\n const envName = match[1];\n if (envName === undefined) return null;\n const resolved = process.env[envName];\n if (resolved === undefined || resolved === '') return null;\n return resolved;\n}\n","/**\n * OpenAI-compatible gateway adapter — talk to any HTTP gateway that exposes\n * the OpenAI Chat Completions API. The gateway may itself be a multi-model\n * router (Bedrock/Vertex/Azure proxy, OpenRouter, vLLM, etc.). nexus-agents\n * sees one adapter, the gateway exposes N models, and the existing routing\n * pipeline picks among them.\n *\n * Source: Issue #2468 (epic #2467 child).\n *\n * Configuration precedence (#2503, child 3 of epic #2500):\n * 1. NEXUS_OPENAI_COMPAT_URL + NEXUS_OPENAI_COMPAT_KEY env vars (both required)\n * 2. NEXUS_OPENCODE_CONFIG path → opencode.json → providers.openai-compat\n * 3. Unconfigured → adapter not built\n *\n * Models are discovered via GET {base}/v1/models at first use. Each model\n * the gateway exposes can be selected by ID; the adapter wraps the existing\n * `OpenAIAdapter` for the actual chat-completions request, so streaming +\n * tool use + the full IModelAdapter contract come for free.\n */\n\nimport OpenAI from 'openai';\n\nimport type {\n Result,\n CompletionRequest,\n CompletionResponse,\n ModelError,\n ModelMetadata,\n IModelAdapter,\n} from '../core/index.js';\nimport { ok, err, ConfigError, getErrorMessage, getTimeProvider } from '../core/index.js';\nimport { OpenAIAdapter } from './openai-adapter.js';\nimport { recordUsageEvent, computeCostUSD } from '../learning/usage-log.js';\nimport { readOpencodeGateway } from '../config/opencode-bridge.js';\n\nexport interface OpenAICompatConfig {\n /** Gateway base URL — must reach `/v1/models` and `/v1/chat/completions`. */\n readonly baseUrl: string;\n /** API key the gateway expects. */\n readonly apiKey: string;\n}\n\nexport interface DiscoveredModel {\n readonly id: string;\n /** Unix epoch seconds when the model was created (per OpenAI API). */\n readonly created?: number;\n /** Owning organization or upstream provider name. */\n readonly ownedBy?: string;\n}\n\n/**\n * Read the gateway config with the precedence chain documented in the\n * module docstring: env vars > opencode.json > unconfigured.\n *\n * The env-var path (#2468) wins when both `NEXUS_OPENAI_COMPAT_URL` and\n * `NEXUS_OPENAI_COMPAT_KEY` are set. Otherwise, when `NEXUS_OPENCODE_CONFIG`\n * names a path, the opencode.json bridge tries to source the gateway from\n * `providers.openai-compat.options.{baseURL, apiKey}` (#2503). Returns\n * `null` when neither path yields a config — caller treats unset gateway\n * as \"no adapter from this source.\"\n */\nexport function readOpenAICompatEnv(): OpenAICompatConfig | null {\n const fromEnv = readGatewayFromEnv();\n if (fromEnv !== null) return fromEnv;\n return readGatewayFromOpencode();\n}\n\nfunction readGatewayFromEnv(): OpenAICompatConfig | null {\n const envUrl = process.env['NEXUS_OPENAI_COMPAT_URL']?.trim();\n const envKey = process.env['NEXUS_OPENAI_COMPAT_KEY']?.trim();\n if (envUrl === undefined || envUrl === '') return null;\n if (envKey === undefined || envKey === '') return null;\n return { baseUrl: envUrl, apiKey: envKey };\n}\n\nfunction readGatewayFromOpencode(): OpenAICompatConfig | null {\n const opencodePath = process.env['NEXUS_OPENCODE_CONFIG']?.trim();\n if (opencodePath === undefined || opencodePath === '') return null;\n const fromFile = readOpencodeGateway(opencodePath);\n if (fromFile === null) return null;\n return { baseUrl: fromFile.baseURL, apiKey: fromFile.apiKey };\n}\n\n/**\n * Discover available models by calling `GET {baseUrl}/v1/models`. Uses the\n * official `openai` SDK's `client.models.list()` so we benefit from its\n * pagination + retry handling. The list is the strongly authoritative\n * source: nexus-agents won't try to dispatch to a model the gateway doesn't\n * expose.\n */\nexport async function discoverModels(\n config: OpenAICompatConfig\n): Promise<Result<readonly DiscoveredModel[], ConfigError>> {\n try {\n const client = new OpenAI({ baseURL: config.baseUrl, apiKey: config.apiKey });\n const list = await client.models.list();\n const models: readonly DiscoveredModel[] = list.data.map((m) => ({\n id: m.id,\n created: m.created,\n ownedBy: m.owned_by,\n }));\n return ok(models);\n } catch (e: unknown) {\n return err(\n new ConfigError(\n `Failed to discover models from ${config.baseUrl}: ${getErrorMessage(e)}. ` +\n `Verify NEXUS_OPENAI_COMPAT_URL and NEXUS_OPENAI_COMPAT_KEY, then retry.`\n )\n );\n }\n}\n\n/**\n * Create an OpenAIAdapter pointed at the gateway for a specific model ID,\n * wrapped with usage recording so every completion appends a UsageEvent\n * to the JSONL log consumed by `nexus-agents usage`.\n *\n * The wrapper is transparent — same IModelAdapter contract, same fields,\n * same error handling. Recording is best-effort (telemetry never fails\n * the user's call).\n *\n * When invoked via MCP, the host harness's model identifier is passed\n * through verbatim — nexus-agents doesn't second-guess what the host is\n * already routing.\n */\nexport function createOpenAICompatAdapter(\n modelId: string,\n config: OpenAICompatConfig\n): IModelAdapter {\n const inner = new OpenAIAdapter({ modelId, apiKey: config.apiKey, baseUrl: config.baseUrl });\n return withUsageRecording(inner);\n}\n\n/**\n * Wrap any IModelAdapter so that successful + failed `complete()` calls\n * append a UsageEvent to the on-disk usage log. Stream calls aren't yet\n * instrumented (a future PR can add streaming-aware recording).\n *\n * The returned object preserves the IModelAdapter contract identically;\n * downstream code can't tell the difference except that one extra JSONL\n * line gets written per call.\n */\nfunction withUsageRecording(inner: IModelAdapter): IModelAdapter {\n const wrapped: IModelAdapter = {\n providerId: inner.providerId,\n modelId: inner.modelId,\n capabilities: inner.capabilities,\n countTokens: (text) => inner.countTokens(text),\n validateConfig: () => inner.validateConfig(),\n stream: (request) => inner.stream(request),\n async complete(request: CompletionRequest): Promise<Result<CompletionResponse, ModelError>> {\n const start = getTimeProvider().now();\n const result = await inner.complete(request);\n const latencyMs = getTimeProvider().now() - start;\n try {\n if (result.ok) {\n const u = result.value.usage;\n recordUsageEvent({\n timestamp: new Date().toISOString(),\n modelId: inner.modelId,\n providerId: inner.providerId,\n inputTokens: u.inputTokens,\n outputTokens: u.outputTokens,\n usdCost: computeCostUSD(inner.modelId, u.inputTokens, u.outputTokens),\n latencyMs,\n success: true,\n });\n } else {\n recordUsageEvent({\n timestamp: new Date().toISOString(),\n modelId: inner.modelId,\n providerId: inner.providerId,\n inputTokens: 0,\n outputTokens: 0,\n usdCost: 0,\n latencyMs,\n success: false,\n errorCode: result.error.code,\n });\n }\n } catch {\n // Telemetry must not break user calls.\n }\n return result;\n },\n };\n attachListModels(wrapped, inner);\n return wrapped;\n}\n\n/**\n * (#2540) Forward `listModels` through the wrapper when the inner adapter\n * exposes one. Only attach when defined so the wrapper's `listModels?:`\n * hint stays accurate for the resolver. The inner reference is captured\n * by closure so the forwarded call binds `this` to the inner adapter.\n */\nfunction attachListModels(wrapped: IModelAdapter, inner: IModelAdapter): void {\n const list = inner.listModels?.bind(inner);\n if (list === undefined) return;\n wrapped.listModels = (): Promise<readonly ModelMetadata[]> => list();\n}\n\n/**\n * Convenience: read env, discover, return adapter instances for every\n * discovered model. Returns `null` (not an error) when env vars aren't set\n * — the caller treats unset gateway as \"no adapter from this source.\"\n *\n * Use case: the unified registry / factory calls this at startup; if the\n * operator has configured a gateway, every discovered model becomes a\n * dispatch target alongside the existing claude/codex/gemini/opencode\n * adapter slots.\n */\nexport async function buildOpenAICompatAdapters(): Promise<Result<\n readonly IModelAdapter[],\n ConfigError\n> | null> {\n const config = readOpenAICompatEnv();\n if (config === null) return null;\n const discovered = await discoverModels(config);\n if (!discovered.ok) return discovered;\n return ok(discovered.value.map((m) => createOpenAICompatAdapter(m.id, config)));\n}\n","/**\n * usage-log — append-only per-call usage events with cost.\n *\n * Source: Issue #2469 (epic #2467 child).\n *\n * For operators running against metered API gateways, per-call cost +\n * tokens + latency is the data they need to manage spend. This module\n * provides three things:\n *\n * 1. `recordUsageEvent(event)` — append a per-call record to a JSONL\n * log under <NEXUS_DATA_DIR>/usage/usage-YYYY-MM.jsonl.\n * 2. `loadUsageEvents({...})` — read events for a window, filtered\n * by model / category.\n * 3. `computeCostUSD(modelId, inputTokens, outputTokens)` — compute\n * cost from `config/in-tree-data.ts` pricing via `lookupInTreeCapability`.\n *\n * The `usage` CLI command (cli/usage-command.ts) consumes this for the\n * operator dashboard. Existing OutcomeStore is intentionally untouched\n * — its schema is for routing/learning signals, not billing.\n */\n\nimport { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync } from 'node:fs';\nimport { dirname, join } from 'node:path';\n\nimport { getNexusDataDir } from '../config/nexus-data-dir.js';\nimport { lookupInTreeCapability } from '../config/model-config-helpers.js';\n\nexport interface UsageEvent {\n /** ISO 8601 timestamp of the call. */\n readonly timestamp: string;\n /** Model identifier (e.g., 'claude-sonnet-4', 'gpt-4o'). */\n readonly modelId: string;\n /** Provider/adapter (e.g., 'anthropic', 'openai', 'openai-compat'). */\n readonly providerId: string;\n /** Token counts. */\n readonly inputTokens: number;\n readonly outputTokens: number;\n /** Cost in USD. Computed at write time from pricing in `config/in-tree-data.ts`. */\n readonly usdCost: number;\n /** Wall-clock latency in milliseconds. */\n readonly latencyMs: number;\n /** Whether the call succeeded. */\n readonly success: boolean;\n /**\n * Optional task category — populated when the call was made on behalf of\n * a routed task (so aggregation can roll up by category).\n */\n readonly category?: string;\n /** Optional failure code when success === false. */\n readonly errorCode?: string;\n}\n\n/**\n * Compute cost in USD given a model and token counts. Returns 0 when the\n * model has no pricing data (e.g., free local model, gateway-routed model\n * we don't have rates for). Operators with custom gateways can extend\n * `config/in-tree-data.ts` to add their pricing.\n */\nexport function computeCostUSD(modelId: string, inputTokens: number, outputTokens: number): number {\n const cap = lookupInTreeCapability(modelId);\n if (cap === undefined) return 0;\n const inputPer1M = cap.pricing?.inputPer1M ?? 0;\n const outputPer1M = cap.pricing?.outputPer1M ?? 0;\n // Multiply token counts by per-million rate then divide. Use Math.round\n // at micro-USD precision so JSONL files don't drift to floating-point\n // noise on small calls.\n const microUsd = Math.round(\n inputTokens * inputPer1M + outputTokens * outputPer1M // micro-USD per million scaled\n );\n return microUsd / 1_000_000;\n}\n\n/** Resolve the active usage log path for the current month. */\nexport function getUsageLogPath(date: Date = new Date()): string {\n const year = date.getUTCFullYear();\n const month = String(date.getUTCMonth() + 1).padStart(2, '0');\n return join(getNexusDataDir(), 'usage', `usage-${String(year)}-${month}.jsonl`);\n}\n\n/**\n * Append a usage event to the current month's log. Best-effort — failures\n * are silent (we don't want to fail a successful model call because we\n * couldn't write a log line).\n */\nexport function recordUsageEvent(event: UsageEvent): void {\n try {\n const path = getUsageLogPath(new Date(event.timestamp));\n mkdirSync(dirname(path), { recursive: true });\n appendFileSync(path, `${JSON.stringify(event)}\\n`, 'utf-8');\n } catch {\n // Intentionally silent — telemetry must not break user calls.\n }\n}\n\nexport interface LoadUsageOptions {\n /** Restrict to events at or after this ISO timestamp. */\n readonly sinceIso?: string;\n /** Restrict to events before this ISO timestamp. */\n readonly untilIso?: string;\n /** Only events for this model. */\n readonly modelId?: string;\n /** Only events for this category. */\n readonly category?: string;\n}\n\nfunction listUsageFiles(dir: string): readonly string[] {\n if (!existsSync(dir)) return [];\n try {\n return readdirSync(dir).filter((f) => f.startsWith('usage-') && f.endsWith('.jsonl'));\n } catch {\n return [];\n }\n}\n\ninterface LoadFilter {\n readonly sinceMs: number;\n readonly untilMs: number;\n readonly modelId: string | undefined;\n readonly category: string | undefined;\n}\n\nfunction eventMatches(parsed: UsageEvent, f: LoadFilter): boolean {\n const ts = Date.parse(parsed.timestamp);\n if (ts < f.sinceMs || ts >= f.untilMs) return false;\n if (f.modelId !== undefined && parsed.modelId !== f.modelId) return false;\n if (f.category !== undefined && parsed.category !== f.category) return false;\n return true;\n}\n\nfunction parseFileLines(filePath: string, filter: LoadFilter): readonly UsageEvent[] {\n let content: string;\n try {\n content = readFileSync(filePath, 'utf-8');\n } catch {\n return [];\n }\n const out: UsageEvent[] = [];\n for (const line of content.split('\\n')) {\n if (line.trim() === '') continue;\n try {\n const parsed = JSON.parse(line) as UsageEvent;\n if (eventMatches(parsed, filter)) out.push(parsed);\n } catch {\n // Skip malformed line; keep reading.\n continue;\n }\n }\n return out;\n}\n\n/**\n * Load all usage events from disk that match the filter. Reads every\n * monthly log file under the data dir; for sub-second filtering at scale\n * a future PR can index by month, but linear scan is fine at the\n * \"operator dashboard\" scale this command targets.\n */\nexport function loadUsageEvents(opts: LoadUsageOptions = {}): readonly UsageEvent[] {\n const dir = join(getNexusDataDir(), 'usage');\n const files = listUsageFiles(dir);\n if (files.length === 0) return [];\n const filter: LoadFilter = {\n sinceMs: opts.sinceIso !== undefined ? Date.parse(opts.sinceIso) : Number.NEGATIVE_INFINITY,\n untilMs: opts.untilIso !== undefined ? Date.parse(opts.untilIso) : Number.POSITIVE_INFINITY,\n modelId: opts.modelId,\n category: opts.category,\n };\n const events: UsageEvent[] = [];\n for (const f of files) {\n events.push(...parseFileLines(join(dir, f), filter));\n }\n return events;\n}\n\nexport interface ModelRollup {\n readonly modelId: string;\n readonly providerId: string;\n readonly callCount: number;\n readonly successCount: number;\n readonly successRate: number;\n readonly totalInputTokens: number;\n readonly totalOutputTokens: number;\n readonly totalUsdCost: number;\n readonly avgLatencyMs: number;\n readonly costPerSuccessUsd: number;\n}\n\n/**\n * Aggregate events into per-model rollups. Sorted by total cost descending\n * — the model burning the most money at top. Useful for \"where is my spend\n * going?\" investigations.\n */\nexport function rollupByModel(events: readonly UsageEvent[]): readonly ModelRollup[] {\n const groups = new Map<string, UsageEvent[]>();\n for (const e of events) {\n const arr = groups.get(e.modelId);\n if (arr === undefined) groups.set(e.modelId, [e]);\n else arr.push(e);\n }\n const rollups: ModelRollup[] = [];\n for (const [modelId, group] of groups) {\n const callCount = group.length;\n const successCount = group.filter((e) => e.success).length;\n const totalInputTokens = group.reduce((s, e) => s + e.inputTokens, 0);\n const totalOutputTokens = group.reduce((s, e) => s + e.outputTokens, 0);\n const totalUsdCost = group.reduce((s, e) => s + e.usdCost, 0);\n const totalLatency = group.reduce((s, e) => s + e.latencyMs, 0);\n const successRate = callCount === 0 ? 0 : successCount / callCount;\n const avgLatencyMs = callCount === 0 ? 0 : totalLatency / callCount;\n const costPerSuccessUsd = successCount === 0 ? totalUsdCost : totalUsdCost / successCount;\n rollups.push({\n modelId,\n providerId: group[0]?.providerId ?? 'unknown',\n callCount,\n successCount,\n successRate,\n totalInputTokens,\n totalOutputTokens,\n totalUsdCost,\n avgLatencyMs,\n costPerSuccessUsd,\n });\n }\n return rollups.sort((a, b) => b.totalUsdCost - a.totalUsdCost);\n}\n"],"mappings":"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","names":["err","readFileSync","readFileSync"]}
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  CliCircuitBreaker,
3
3
  DEFAULT_CIRCUIT_BREAKER_CONFIG
4
- } from "./chunk-PWTJGGKB.js";
4
+ } from "./chunk-DLXT23AC.js";
5
5
  import {
6
6
  CLI_SUBPROCESS_TIMEOUTS,
7
7
  CLI_TIMEOUTS,
@@ -24,7 +24,7 @@ import {
24
24
  getTimeProvider,
25
25
  isRateLimitText,
26
26
  ok
27
- } from "./chunk-3MRM53T4.js";
27
+ } from "./chunk-WDYCIJWN.js";
28
28
 
29
29
  // src/cli-adapters/subprocess-adapter.ts
30
30
  import { spawn } from "child_process";
@@ -3062,4 +3062,4 @@ export {
3062
3062
  isCliAvailable,
3063
3063
  getAvailableClis
3064
3064
  };
3065
- //# sourceMappingURL=chunk-TDV5ALHY.js.map
3065
+ //# sourceMappingURL=chunk-D6TM2VHX.js.map
@@ -5,7 +5,7 @@ import {
5
5
  getErrorMessage,
6
6
  getTimeProvider,
7
7
  ok
8
- } from "./chunk-3MRM53T4.js";
8
+ } from "./chunk-WDYCIJWN.js";
9
9
 
10
10
  // src/cli-adapters/circuit-breaker-types.ts
11
11
  var CircuitErrorCode = {
@@ -353,4 +353,4 @@ export {
353
353
  CircuitBreakerRegistry,
354
354
  mapCliErrorToCategory
355
355
  };
356
- //# sourceMappingURL=chunk-PWTJGGKB.js.map
356
+ //# sourceMappingURL=chunk-DLXT23AC.js.map
@@ -0,0 +1,276 @@
1
+ import {
2
+ getToolMemory
3
+ } from "./chunk-Q5CFPIJ5.js";
4
+ import {
5
+ CLI_NAMES,
6
+ StrategyDistiller,
7
+ createLogger,
8
+ getOutcomeStore,
9
+ registerPersistentDistillerFactory
10
+ } from "./chunk-WDYCIJWN.js";
11
+ import {
12
+ ensureLearningDir,
13
+ getRulesFile
14
+ } from "./chunk-7BMOZJYS.js";
15
+
16
+ // src/learning/strategy-distiller-persistence.ts
17
+ import { writeFileSync, readFileSync, renameSync, unlinkSync, existsSync } from "fs";
18
+ import { dirname } from "path";
19
+ import { z } from "zod";
20
+ var DistilledRuleSchema = z.object({
21
+ id: z.string(),
22
+ patternType: z.enum(["failure-rate", "success-rate", "latency-spike"]),
23
+ cli: z.enum(CLI_NAMES),
24
+ category: z.string(),
25
+ action: z.enum(["penalize", "boost", "avoid"]),
26
+ confidence: z.number(),
27
+ observationCount: z.number(),
28
+ metric: z.number(),
29
+ status: z.enum(["draft", "active", "promoted", "expired"]),
30
+ createdAt: z.number(),
31
+ updatedAt: z.number(),
32
+ tainted: z.boolean()
33
+ });
34
+ var RulesSnapshotSchema = z.object({
35
+ version: z.literal(1),
36
+ savedAt: z.string(),
37
+ rules: z.array(DistilledRuleSchema)
38
+ });
39
+ var PersistentStrategyDistiller = class extends StrategyDistiller {
40
+ filePath;
41
+ persistLogger;
42
+ constructor(outcomeStore, persistConfig, logger, distillerConfig) {
43
+ super(outcomeStore, logger, distillerConfig);
44
+ this.filePath = persistConfig?.filePath ?? getRulesFile();
45
+ this.persistLogger = logger ?? createLogger({ component: "PersistentStrategyDistiller" });
46
+ const dataDir = persistConfig?.dataDir;
47
+ ensureLearningDir(dataDir);
48
+ this.hydrate();
49
+ }
50
+ /** Override distill to persist rules after each run. */
51
+ distill() {
52
+ super.distill();
53
+ this.saveSnapshot();
54
+ }
55
+ // ==========================================================================
56
+ // Private
57
+ // ==========================================================================
58
+ hydrate() {
59
+ if (!existsSync(this.filePath)) {
60
+ this.persistLogger.debug("No rules file found, starting fresh", {
61
+ path: this.filePath
62
+ });
63
+ return;
64
+ }
65
+ try {
66
+ const content = readFileSync(this.filePath, "utf-8");
67
+ const parsed = JSON.parse(content);
68
+ const result = RulesSnapshotSchema.safeParse(parsed);
69
+ if (!result.success) {
70
+ this.persistLogger.warn("Rules file failed validation, starting fresh", {
71
+ path: this.filePath,
72
+ error: result.error.message
73
+ });
74
+ return;
75
+ }
76
+ this.loadRules(result.data.rules);
77
+ this.persistLogger.info("Hydrated distilled rules from disk", {
78
+ ruleCount: result.data.rules.length,
79
+ savedAt: result.data.savedAt,
80
+ path: this.filePath
81
+ });
82
+ } catch (error) {
83
+ const msg = error instanceof Error ? error.message : String(error);
84
+ this.persistLogger.warn("Failed to hydrate rules from disk", {
85
+ error: msg,
86
+ path: this.filePath
87
+ });
88
+ }
89
+ }
90
+ saveSnapshot() {
91
+ const rules = this.getRules();
92
+ const snapshot = {
93
+ version: 1,
94
+ savedAt: (/* @__PURE__ */ new Date()).toISOString(),
95
+ rules
96
+ };
97
+ const tmpPath = this.filePath + ".tmp";
98
+ try {
99
+ ensureLearningDir(dirname(this.filePath));
100
+ writeFileSync(tmpPath, JSON.stringify(snapshot, null, 2), "utf-8");
101
+ renameSync(tmpPath, this.filePath);
102
+ } catch (error) {
103
+ const msg = error instanceof Error ? error.message : String(error);
104
+ this.persistLogger.warn("Failed to persist rules to disk", {
105
+ error: msg,
106
+ path: this.filePath
107
+ });
108
+ try {
109
+ if (existsSync(tmpPath)) unlinkSync(tmpPath);
110
+ } catch (cleanupErr) {
111
+ const msg2 = cleanupErr instanceof Error ? cleanupErr.message : String(cleanupErr);
112
+ this.persistLogger.debug("Failed to clean up temp file during persist", {
113
+ path: tmpPath,
114
+ error: msg2
115
+ });
116
+ }
117
+ }
118
+ }
119
+ };
120
+ registerPersistentDistillerFactory(
121
+ (outcomeStore, logger) => new PersistentStrategyDistiller(outcomeStore, void 0, logger)
122
+ );
123
+ function loadPersistedRules(filePath = getRulesFile()) {
124
+ if (!existsSync(filePath)) return [];
125
+ try {
126
+ const content = readFileSync(filePath, "utf-8");
127
+ const parsed = JSON.parse(content);
128
+ const result = RulesSnapshotSchema.safeParse(parsed);
129
+ if (!result.success) return [];
130
+ return result.data.rules;
131
+ } catch {
132
+ return [];
133
+ }
134
+ }
135
+
136
+ // src/context/context-retriever.ts
137
+ var DEFAULT_LIMIT = 5;
138
+ async function getContextForTask(options) {
139
+ const limit = options.limit ?? DEFAULT_LIMIT;
140
+ const logger = options.logger ?? createLogger({ component: "ContextRetriever" });
141
+ const [beliefs, similarMemories, recentLearnings, experiencePatterns, outcomes] = await Promise.all([
142
+ fetchBeliefs(options.task, limit, logger),
143
+ fetchSimilarMemories(options.task, limit, logger),
144
+ fetchRecentLearnings(options.task, limit, logger),
145
+ fetchExperiencePatterns(options.task, limit, logger),
146
+ fetchOutcomes(options.category, logger)
147
+ ]);
148
+ const priorStrategies = fetchPriorStrategies(options.category, limit, logger);
149
+ return {
150
+ beliefs,
151
+ similarMemories,
152
+ recentLearnings,
153
+ experiencePatterns,
154
+ outcomes,
155
+ priorStrategies
156
+ };
157
+ }
158
+ function fetchPriorStrategies(category, limit, logger) {
159
+ try {
160
+ const all = loadPersistedRules();
161
+ return all.filter((r) => r.status === "active" && !r.tainted).filter((r) => r.category === category || r.category === "*").slice(0, limit);
162
+ } catch (error) {
163
+ logger.debug("ContextRetriever: prior-strategies fetch failed", {
164
+ error: formatError(error)
165
+ });
166
+ return [];
167
+ }
168
+ }
169
+ async function fetchBeliefs(task, limit, logger) {
170
+ try {
171
+ const tm = getToolMemory(logger);
172
+ const beliefs = tm.getBeliefMemory();
173
+ const result = await beliefs.recallBySubject(task, limit);
174
+ return result.ok ? result.value : [];
175
+ } catch (error) {
176
+ logger.debug("ContextRetriever: belief fetch failed", { error: formatError(error) });
177
+ return [];
178
+ }
179
+ }
180
+ async function fetchSimilarMemories(task, limit, logger) {
181
+ try {
182
+ const tm = getToolMemory(logger);
183
+ const agentic = tm.getAgenticMemoryBackend();
184
+ if (agentic === null) return [];
185
+ const result = await agentic.searchAgentic(task, limit);
186
+ return result.ok ? result.value : [];
187
+ } catch (error) {
188
+ logger.debug("ContextRetriever: agentic search failed", { error: formatError(error) });
189
+ return [];
190
+ }
191
+ }
192
+ async function fetchRecentLearnings(task, limit, logger) {
193
+ try {
194
+ const tm = getToolMemory(logger);
195
+ const adaptive = tm.getAdaptiveMemoryBackend();
196
+ if (adaptive === null) return [];
197
+ const result = await adaptive.retrieveByPriority({ query: task, limit });
198
+ return result.ok ? result.value : [];
199
+ } catch (error) {
200
+ logger.debug("ContextRetriever: adaptive fetch failed", { error: formatError(error) });
201
+ return [];
202
+ }
203
+ }
204
+ function fetchExperiencePatterns(task, limit, logger) {
205
+ try {
206
+ const tm = getToolMemory(logger);
207
+ const mobimem = tm.getMobiMem();
208
+ if (mobimem === null) return Promise.resolve([]);
209
+ return Promise.resolve(mobimem.experience.findPatterns(task, limit));
210
+ } catch (error) {
211
+ logger.debug("ContextRetriever: mobimem fetch failed", { error: formatError(error) });
212
+ return Promise.resolve([]);
213
+ }
214
+ }
215
+ function fetchOutcomes(category, logger) {
216
+ try {
217
+ const store = getOutcomeStore();
218
+ return Promise.resolve(store.summarize({ category }));
219
+ } catch (error) {
220
+ logger.debug("ContextRetriever: outcomes fetch failed", { error: formatError(error) });
221
+ return Promise.resolve(null);
222
+ }
223
+ }
224
+ function formatError(error) {
225
+ return error instanceof Error ? error.message : String(error);
226
+ }
227
+ function inferTaskCategory(task) {
228
+ const t = task.toLowerCase();
229
+ if (/security|vulnerab|cve|threat|owasp|injection|xss/.test(t)) return "security_review";
230
+ if (/architect|design doc|rfc|adr|system design/.test(t)) return "architecture";
231
+ if (/test|spec|coverage|vitest|jest|pytest/.test(t)) return "testing";
232
+ if (/review|audit|critique|feedback/.test(t)) return "code_review";
233
+ if (/docs|documentation|readme|tutorial|guide/.test(t)) return "documentation";
234
+ if (/plan|roadmap|epic|sprint|breakdown/.test(t)) return "planning";
235
+ if (/research|investigate|explore|survey|analyze/.test(t)) return "research";
236
+ if (/deploy| ci |\bcd\b|pipeline|kubernetes|docker|infra|terraform/.test(t)) return "devops";
237
+ if (/implement|build|create|add|refactor|fix|bug|feature/.test(t)) return "code_generation";
238
+ return "exploration";
239
+ }
240
+ function summarizeContextForPrompt(ctx) {
241
+ const sections = [];
242
+ if (ctx.beliefs.length > 0) {
243
+ const lines = ctx.beliefs.slice(0, 5).map((b) => `- ${b.subject} ${b.predicate} ${b.object} (confidence: ${b.confidence})`);
244
+ sections.push(`### Beliefs
245
+ ${lines.join("\n")}`);
246
+ }
247
+ if (ctx.similarMemories.length > 0) {
248
+ const lines = ctx.similarMemories.slice(0, 3).map((m) => `- ${m.attributes.contextDescription}`);
249
+ sections.push(`### Similar prior work
250
+ ${lines.join("\n")}`);
251
+ }
252
+ if (ctx.experiencePatterns.length > 0) {
253
+ const lines = ctx.experiencePatterns.slice(0, 3).map(
254
+ (p) => `- ${p.taskType}: ${(p.successRate * 100).toFixed(0)}% success over ${String(p.attemptCount)} attempts`
255
+ );
256
+ sections.push(`### Observed patterns
257
+ ${lines.join("\n")}`);
258
+ }
259
+ if (ctx.outcomes !== null && ctx.outcomes.totalTasks > 0) {
260
+ sections.push(
261
+ `### Outcomes for this category
262
+ - ${String(ctx.outcomes.totalTasks)} prior tasks, ${(ctx.outcomes.successRate * 100).toFixed(0)}% success`
263
+ );
264
+ }
265
+ return sections.length === 0 ? "" : `## Prior Context (Nexus Memory)
266
+ ${sections.join("\n\n")}`;
267
+ }
268
+
269
+ export {
270
+ RulesSnapshotSchema,
271
+ PersistentStrategyDistiller,
272
+ getContextForTask,
273
+ inferTaskCategory,
274
+ summarizeContextForPrompt
275
+ };
276
+ //# sourceMappingURL=chunk-DNO2INX5.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/learning/strategy-distiller-persistence.ts","../src/context/context-retriever.ts"],"sourcesContent":["/**\n * Persistent StrategyDistiller — JSON-backed cross-session persistence.\n *\n * Extends StrategyDistiller with atomic disk writes (write tmp + rename)\n * for distilled rules. Hydrates from a versioned JSON snapshot on\n * construction; saves after every distill() call.\n *\n * @module learning/strategy-distiller-persistence\n * (Source: Issue #1009 — Cross-session persistence)\n */\n\nimport { writeFileSync, readFileSync, renameSync, unlinkSync, existsSync } from 'node:fs';\nimport { dirname } from 'node:path';\nimport { z } from 'zod';\nimport { CLI_NAMES } from '../config/model-capabilities-types.js';\n\nimport type { ILogger } from '../core/index.js';\nimport { createLogger } from '../core/index.js';\nimport type { OutcomeStore } from '../orchestration/outcomes/outcome-store.js';\nimport type { DistilledRule, DistillerConfig } from './strategy-distiller-types.js';\nimport { StrategyDistiller, registerPersistentDistillerFactory } from './strategy-distiller.js';\nimport { ensureLearningDir, getRulesFile } from '../config/learning-persistence.js';\n\n// ============================================================================\n// Versioned Schema\n// ============================================================================\n\nconst DistilledRuleSchema = z.object({\n id: z.string(),\n patternType: z.enum(['failure-rate', 'success-rate', 'latency-spike']),\n cli: z.enum(CLI_NAMES),\n category: z.string(),\n action: z.enum(['penalize', 'boost', 'avoid']),\n confidence: z.number(),\n observationCount: z.number(),\n metric: z.number(),\n status: z.enum(['draft', 'active', 'promoted', 'expired']),\n createdAt: z.number(),\n updatedAt: z.number(),\n tainted: z.boolean(),\n});\n\n/** Versioned snapshot schema for atomic saves. */\nexport const RulesSnapshotSchema = z.object({\n version: z.literal(1),\n savedAt: z.string(),\n rules: z.array(DistilledRuleSchema),\n});\n\nexport type RulesSnapshot = z.infer<typeof RulesSnapshotSchema>;\n\n// ============================================================================\n// Configuration\n// ============================================================================\n\nexport interface PersistentDistillerConfig {\n /** Override the file path (useful for testing). */\n readonly filePath?: string;\n /** Override the data directory (useful for testing). */\n readonly dataDir?: string;\n}\n\n// ============================================================================\n// Implementation\n// ============================================================================\n\n/**\n * StrategyDistiller that persists distilled rules to a JSON file.\n *\n * - Construction: hydrates from rules.json via Zod validation\n * - distill(): calls super.distill() then atomically saves snapshot\n * - Corruption: warn + start fresh (no partial loads)\n */\nexport class PersistentStrategyDistiller extends StrategyDistiller {\n private readonly filePath: string;\n private readonly persistLogger: ILogger;\n\n constructor(\n outcomeStore: OutcomeStore,\n persistConfig?: PersistentDistillerConfig,\n logger?: ILogger,\n distillerConfig?: Partial<DistillerConfig>\n ) {\n super(outcomeStore, logger, distillerConfig);\n this.filePath = persistConfig?.filePath ?? getRulesFile();\n this.persistLogger = logger ?? createLogger({ component: 'PersistentStrategyDistiller' });\n\n const dataDir = persistConfig?.dataDir;\n ensureLearningDir(dataDir);\n this.hydrate();\n }\n\n /** Override distill to persist rules after each run. */\n override distill(): void {\n super.distill();\n this.saveSnapshot();\n }\n\n // ==========================================================================\n // Private\n // ==========================================================================\n\n private hydrate(): void {\n if (!existsSync(this.filePath)) {\n this.persistLogger.debug('No rules file found, starting fresh', {\n path: this.filePath,\n });\n return;\n }\n\n try {\n const content = readFileSync(this.filePath, 'utf-8');\n const parsed: unknown = JSON.parse(content);\n const result = RulesSnapshotSchema.safeParse(parsed);\n\n if (!result.success) {\n this.persistLogger.warn('Rules file failed validation, starting fresh', {\n path: this.filePath,\n error: result.error.message,\n });\n return;\n }\n\n this.loadRules(result.data.rules);\n this.persistLogger.info('Hydrated distilled rules from disk', {\n ruleCount: result.data.rules.length,\n savedAt: result.data.savedAt,\n path: this.filePath,\n });\n } catch (error: unknown) {\n const msg = error instanceof Error ? error.message : String(error);\n this.persistLogger.warn('Failed to hydrate rules from disk', {\n error: msg,\n path: this.filePath,\n });\n }\n }\n\n private saveSnapshot(): void {\n const rules = this.getRules();\n const snapshot: RulesSnapshot = {\n version: 1,\n savedAt: new Date().toISOString(),\n rules: rules as DistilledRule[],\n };\n\n const tmpPath = this.filePath + '.tmp';\n try {\n // Ensure parent directory exists\n ensureLearningDir(dirname(this.filePath));\n // Atomic write: temp file + rename\n writeFileSync(tmpPath, JSON.stringify(snapshot, null, 2), 'utf-8');\n renameSync(tmpPath, this.filePath);\n } catch (error: unknown) {\n const msg = error instanceof Error ? error.message : String(error);\n this.persistLogger.warn('Failed to persist rules to disk', {\n error: msg,\n path: this.filePath,\n });\n // Clean up temp file on failure\n try {\n if (existsSync(tmpPath)) unlinkSync(tmpPath);\n } catch (cleanupErr: unknown) {\n const msg = cleanupErr instanceof Error ? cleanupErr.message : String(cleanupErr);\n this.persistLogger.debug('Failed to clean up temp file during persist', {\n path: tmpPath,\n error: msg,\n });\n }\n }\n }\n}\n\n// Self-register factory so CompositeRouter can create PersistentStrategyDistiller\n// without a circular top-level import.\nregisterPersistentDistillerFactory(\n (outcomeStore, logger) => new PersistentStrategyDistiller(outcomeStore, undefined, logger)\n);\n\n/**\n * Phase 5 of #2792 — read the persisted distilled rules from disk\n * without needing a live `StrategyDistiller` instance.\n *\n * `ContextRetriever` uses this so `UnifiedContext.priorStrategies`\n * surfaces the routing learnings the CompositeRouter has already\n * derived, even when the consumer is in a different process / scope\n * (e.g. an `orchestrate` invocation that hasn't constructed its own\n * router yet).\n *\n * Returns `[]` when the file is missing, corrupt, or unreadable; never\n * throws. The caller is responsible for filtering to status / category /\n * tainted as appropriate — this loader returns the raw rule set so\n * future consumers can apply their own predicates.\n */\nexport function loadPersistedRules(filePath: string = getRulesFile()): readonly DistilledRule[] {\n if (!existsSync(filePath)) return [];\n try {\n const content = readFileSync(filePath, 'utf-8');\n const parsed: unknown = JSON.parse(content);\n const result = RulesSnapshotSchema.safeParse(parsed);\n if (!result.success) return [];\n return result.data.rules;\n } catch {\n // Disk read / parse failures contribute an empty list — the consumer\n // contract is \"absence means no signal,\" never an exception.\n return [];\n }\n}\n","/**\n * ContextRetriever — the unified read surface every entry point should call\n * to learn what we already know about a task.\n *\n * Phase 2 of #2792. Before this module, each entry point (routing, orchestration,\n * graph workflow, expert creation) reinvented memory access — or, more commonly,\n * skipped it entirely. That made every backend write-only in practice.\n *\n * `getContextForTask({ task, category })` is the one function every entry point\n * calls. It fans out across the shared backends in parallel, tolerates\n * individual failures (never throws), and returns a typed `UnifiedContext`\n * the consumer can either use directly or summarize into a prompt.\n *\n * **Implementation choice — typed singletons, not registry fan-out.**\n * Phase 1 (#2793) made `IMemoryBackend.query()` real on every attached\n * domain, so a registry-level `Promise.all(...domains.map(d => d.query()))`\n * would now work. But the result type is `unknown[]` per domain, which\n * loses the typed shapes consumers want. For typed reads, reaching into\n * `getToolMemory()` and `getOutcomeStore()` directly is cleaner. The\n * registry-level fan-out remains the right path for opaque/observability\n * consumers like `memory_stats`.\n *\n * @module context/context-retriever\n * (Source: #2792 / #2794)\n */\n\nimport type { Belief } from './belief-core-types.js';\nimport type { AgenticMemoryEntry } from './agentic-memory.js';\nimport type { ScoredMemoryEntry } from './adaptive-memory-types.js';\nimport type { ExperienceEntry } from './mobimem-types.js';\nimport type { PerformanceSummary } from '../orchestration/outcomes/outcome-types.js';\nimport type { TaskCategory } from '../config/task-specialization-types.js';\nimport type { DistilledRule } from '../learning/strategy-distiller-types.js';\nimport type { ILogger } from '../core/logger.js';\nimport { createLogger } from '../core/logger.js';\nimport { getToolMemory } from '../mcp/tools/tool-memory.js';\nimport { getOutcomeStore } from '../orchestration/outcomes/outcome-store.js';\nimport { loadPersistedRules } from '../learning/strategy-distiller-persistence.js';\n\n/**\n * What we know about a task, derived from every shared memory backend.\n *\n * Every field is `readonly` and may be empty — consumers should treat\n * absence as \"no signal,\" not failure. Errors fetching any single backend\n * are logged and produce empty results for that field; the function as a\n * whole never throws.\n */\nexport interface UnifiedContext {\n /** Beliefs whose subject matches the task text. */\n readonly beliefs: readonly Belief[];\n /** A-MEM Zettelkasten entries similar to the task. */\n readonly similarMemories: readonly AgenticMemoryEntry[];\n /** Adaptive priority-scored entries ranked by relevance + recency + importance. */\n readonly recentLearnings: readonly ScoredMemoryEntry[];\n /** MobiMem patterns observed for the task type. */\n readonly experiencePatterns: readonly ExperienceEntry[];\n /** Performance summary scoped to the requested category. */\n readonly outcomes: PerformanceSummary | null;\n /** Distilled routing rules — populated once #2797 lands; empty until then. */\n readonly priorStrategies: readonly DistilledRule[];\n}\n\n/** Options accepted by {@link getContextForTask}. */\nexport interface ContextRetrieverOptions {\n /** Free-text description of the task. Used as the search term. */\n readonly task: string;\n /** Canonical category for outcome scoping. */\n readonly category: TaskCategory;\n /** Per-backend cap on returned rows. Defaults to 5. */\n readonly limit?: number;\n /** Optional logger override. */\n readonly logger?: ILogger;\n}\n\n/** Sensible default — small enough to embed in a prompt, large enough to be useful. */\nconst DEFAULT_LIMIT = 5;\n\n/**\n * The canonical \"what do we already know about this task\" read.\n *\n * Wire this at the top of every entry point: `CompositeRouter.route`,\n * `orchestrate`, graph workflow start, `create_expert`. Even if the\n * consumer initially just logs the result, every backend's read path\n * gets exercised and the silos visibly converge.\n *\n * Latency: O(slowest individual backend). Each backend's call is wrapped\n * so a slow/failing one doesn't block the others. No caching in this\n * version — if a hot caller emerges, layer a TTL cache on top.\n */\nexport async function getContextForTask(options: ContextRetrieverOptions): Promise<UnifiedContext> {\n const limit = options.limit ?? DEFAULT_LIMIT;\n const logger = options.logger ?? createLogger({ component: 'ContextRetriever' });\n\n const [beliefs, similarMemories, recentLearnings, experiencePatterns, outcomes] =\n await Promise.all([\n fetchBeliefs(options.task, limit, logger),\n fetchSimilarMemories(options.task, limit, logger),\n fetchRecentLearnings(options.task, limit, logger),\n fetchExperiencePatterns(options.task, limit, logger),\n fetchOutcomes(options.category, logger),\n ]);\n\n const priorStrategies = fetchPriorStrategies(options.category, limit, logger);\n\n return {\n beliefs,\n similarMemories,\n recentLearnings,\n experiencePatterns,\n outcomes,\n priorStrategies,\n };\n}\n\n/**\n * Phase 5 of #2792 — surface distilled routing rules in the unified\n * context. Reads from the persisted rules file (written by\n * `PersistentStrategyDistiller`) so consumers see the same learnings the\n * CompositeRouter applies at decision time, without needing a live\n * router instance.\n *\n * Filters to (a) `status === 'active'` (rules that aren't deprecated or\n * shadowed), (b) `tainted === false` (security gate — tainted rules\n * never reach consumers per Phase 5 acceptance), and (c) category\n * matching the task's category or a global rule.\n */\nfunction fetchPriorStrategies(\n category: TaskCategory,\n limit: number,\n logger: ILogger\n): readonly DistilledRule[] {\n try {\n const all = loadPersistedRules();\n return all\n .filter((r) => r.status === 'active' && !r.tainted)\n .filter((r) => r.category === category || r.category === '*')\n .slice(0, limit);\n } catch (error: unknown) {\n logger.debug('ContextRetriever: prior-strategies fetch failed', {\n error: formatError(error),\n });\n return [];\n }\n}\n\nasync function fetchBeliefs(\n task: string,\n limit: number,\n logger: ILogger\n): Promise<readonly Belief[]> {\n try {\n const tm = getToolMemory(logger);\n const beliefs = tm.getBeliefMemory();\n const result = await beliefs.recallBySubject(task, limit);\n return result.ok ? result.value : [];\n } catch (error: unknown) {\n logger.debug('ContextRetriever: belief fetch failed', { error: formatError(error) });\n return [];\n }\n}\n\nasync function fetchSimilarMemories(\n task: string,\n limit: number,\n logger: ILogger\n): Promise<readonly AgenticMemoryEntry[]> {\n try {\n const tm = getToolMemory(logger);\n const agentic = tm.getAgenticMemoryBackend();\n if (agentic === null) return [];\n const result = await agentic.searchAgentic(task, limit);\n return result.ok ? result.value : [];\n } catch (error: unknown) {\n logger.debug('ContextRetriever: agentic search failed', { error: formatError(error) });\n return [];\n }\n}\n\nasync function fetchRecentLearnings(\n task: string,\n limit: number,\n logger: ILogger\n): Promise<readonly ScoredMemoryEntry[]> {\n try {\n const tm = getToolMemory(logger);\n const adaptive = tm.getAdaptiveMemoryBackend();\n if (adaptive === null) return [];\n const result = await adaptive.retrieveByPriority({ query: task, limit });\n return result.ok ? result.value : [];\n } catch (error: unknown) {\n logger.debug('ContextRetriever: adaptive fetch failed', { error: formatError(error) });\n return [];\n }\n}\n\nfunction fetchExperiencePatterns(\n task: string,\n limit: number,\n logger: ILogger\n): Promise<readonly ExperienceEntry[]> {\n try {\n const tm = getToolMemory(logger);\n const mobimem = tm.getMobiMem();\n if (mobimem === null) return Promise.resolve([]);\n return Promise.resolve(mobimem.experience.findPatterns(task, limit));\n } catch (error: unknown) {\n logger.debug('ContextRetriever: mobimem fetch failed', { error: formatError(error) });\n return Promise.resolve([]);\n }\n}\n\nfunction fetchOutcomes(\n category: TaskCategory,\n logger: ILogger\n): Promise<PerformanceSummary | null> {\n try {\n const store = getOutcomeStore();\n return Promise.resolve(store.summarize({ category }));\n } catch (error: unknown) {\n logger.debug('ContextRetriever: outcomes fetch failed', { error: formatError(error) });\n return Promise.resolve(null);\n }\n}\n\nfunction formatError(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n\n/**\n * Best-effort {@link TaskCategory} inference from free-text task content.\n * Used by entry-point wiring (Phase 3 / #2795) when the caller doesn't\n * carry a structured category. Keyword-based; if nothing matches,\n * returns `'exploration'` (which scopes the outcomes summary to the\n * broadest historical baseline).\n *\n * Intentionally simple — this is a *fallback*, not a classifier. Real\n * classification happens at routing time via `cli-adapters/task-classifier`.\n */\nexport function inferTaskCategory(task: string): TaskCategory {\n const t = task.toLowerCase();\n if (/security|vulnerab|cve|threat|owasp|injection|xss/.test(t)) return 'security_review';\n if (/architect|design doc|rfc|adr|system design/.test(t)) return 'architecture';\n if (/test|spec|coverage|vitest|jest|pytest/.test(t)) return 'testing';\n if (/review|audit|critique|feedback/.test(t)) return 'code_review';\n if (/docs|documentation|readme|tutorial|guide/.test(t)) return 'documentation';\n if (/plan|roadmap|epic|sprint|breakdown/.test(t)) return 'planning';\n if (/research|investigate|explore|survey|analyze/.test(t)) return 'research';\n if (/deploy| ci |\\bcd\\b|pipeline|kubernetes|docker|infra|terraform/.test(t)) return 'devops';\n if (/implement|build|create|add|refactor|fix|bug|feature/.test(t)) return 'code_generation';\n return 'exploration';\n}\n\n/**\n * Project a {@link UnifiedContext} into a compact human-readable block\n * suitable for prepending to a system prompt. Skips empty sections so\n * the prefix never wastes tokens on \\\"no signal.\\\"\n *\n * Phase 3 of #2792 — used by `orchestrate` and graph workflow start to\n * surface accumulated memory at the entry point.\n */\nexport function summarizeContextForPrompt(ctx: UnifiedContext): string {\n const sections: string[] = [];\n\n if (ctx.beliefs.length > 0) {\n const lines = ctx.beliefs\n .slice(0, 5)\n .map((b) => `- ${b.subject} ${b.predicate} ${b.object} (confidence: ${b.confidence})`);\n sections.push(`### Beliefs\\n${lines.join('\\n')}`);\n }\n\n if (ctx.similarMemories.length > 0) {\n const lines = ctx.similarMemories\n .slice(0, 3)\n .map((m) => `- ${m.attributes.contextDescription}`);\n sections.push(`### Similar prior work\\n${lines.join('\\n')}`);\n }\n\n if (ctx.experiencePatterns.length > 0) {\n const lines = ctx.experiencePatterns\n .slice(0, 3)\n .map(\n (p) =>\n `- ${p.taskType}: ${(p.successRate * 100).toFixed(0)}% success over ${String(p.attemptCount)} attempts`\n );\n sections.push(`### Observed patterns\\n${lines.join('\\n')}`);\n }\n\n if (ctx.outcomes !== null && ctx.outcomes.totalTasks > 0) {\n sections.push(\n `### Outcomes for this category\\n- ${String(ctx.outcomes.totalTasks)} prior tasks, ${(ctx.outcomes.successRate * 100).toFixed(0)}% success`\n );\n }\n\n return sections.length === 0 ? '' : `## Prior Context (Nexus Memory)\\n${sections.join('\\n\\n')}`;\n}\n"],"mappings":"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","names":["msg"]}
@@ -4,7 +4,7 @@ import {
4
4
  import {
5
5
  createLogger,
6
6
  getTimeProvider
7
- } from "./chunk-3MRM53T4.js";
7
+ } from "./chunk-WDYCIJWN.js";
8
8
 
9
9
  // src/mcp/tools/scanner-registry-fetcher.ts
10
10
  import { z } from "zod";
@@ -501,7 +501,28 @@ var CI_SNIPPETS = {
501
501
  checkov: "- uses: bridgecrewio/checkov-action@master",
502
502
  "osv-scanner": "- uses: google/osv-scanner-action@v1",
503
503
  snyk: "- uses: snyk/actions/node@master # adjust for language",
504
- shellcheck: "- uses: ludeeus/action-shellcheck@master"
504
+ shellcheck: "- uses: ludeeus/action-shellcheck@master",
505
+ // #2732: scanners recommended for TypeScript repos that were missing
506
+ // snippets, leaving most rows with ciSnippet: null.
507
+ "eslint-security": "- run: npm install --save-dev eslint-plugin-security && npx eslint --plugin security .",
508
+ sonarqube: "- uses: sonarsource/sonarqube-scan-action@v3\n env:\n SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}",
509
+ "npm-audit": "- run: npm audit --audit-level=high",
510
+ trivy: "- uses: aquasecurity/trivy-action@master\n with:\n scan-type: fs\n severity: CRITICAL,HIGH\n exit-code: 1",
511
+ trufflehog: "- uses: trufflesecurity/trufflehog@main\n with:\n extra_args: --only-verified",
512
+ cppcheck: "- run: cppcheck --enable=all --error-exitcode=1 .",
513
+ spotbugs: "- uses: jwgmeligmeyling/spotbugs-github-action@master\n with:\n path: target/spotbugsXml.xml",
514
+ "pip-audit": "- run: pip install pip-audit && pip-audit",
515
+ "cargo-audit": "- run: cargo install cargo-audit --locked && cargo audit",
516
+ "bundler-audit": "- run: gem install bundler-audit && bundle-audit check --update",
517
+ "composer-audit": "- run: composer audit",
518
+ govulncheck: "- run: go install golang.org/x/vuln/cmd/govulncheck@latest && govulncheck ./...",
519
+ detekt: "- uses: natiginfo/action-detekt-all@1.23.6",
520
+ brakeman: "- run: gem install brakeman && brakeman -f json",
521
+ phpstan: "- run: composer install && vendor/bin/phpstan analyse",
522
+ tfsec: "- uses: aquasecurity/tfsec-action@v1.0.3",
523
+ "owasp-dependency-check": "- uses: dependency-check/Dependency-Check_Action@main\n with:\n project: ${{ github.event.repository.name }}\n path: .\n format: HTML",
524
+ "owasp-zap": "- uses: zaproxy/action-baseline@v0.12.0\n with:\n target: ${{ env.TARGET_URL }}",
525
+ syft: "- uses: anchore/sbom-action@v0\n with:\n path: .\n format: cyclonedx-json"
505
526
  };
506
527
  function generateCiSnippet(name, ci) {
507
528
  if (ci !== "github-actions") return null;
@@ -514,13 +535,15 @@ function isAlreadyUsed(name, existing) {
514
535
  return existing.some((t) => t.toLowerCase().includes(name.toLowerCase()));
515
536
  }
516
537
  function collectCategoryRecs(recs, opts) {
538
+ let primarySlotFilled = recs.some((r) => r.category === opts.category);
517
539
  for (const name of opts.names) {
518
540
  if (recs.length >= opts.ctx.maxScanners) break;
519
541
  if (isAlreadyUsed(name, opts.ctx.existing)) continue;
520
542
  const entry = findScanner(name, opts.ctx.scanners);
521
543
  if (!entry) continue;
522
544
  if (opts.ctx.categoryFilter && !opts.ctx.categoryFilter.has(opts.category)) continue;
523
- const isFirst = opts.category === "sast" && recs.length === 0;
545
+ const priority = primarySlotFilled ? "recommended" : opts.priority;
546
+ primarySlotFilled = true;
524
547
  recs.push({
525
548
  name,
526
549
  displayName: entry.displayName,
@@ -528,7 +551,7 @@ function collectCategoryRecs(recs, opts) {
528
551
  license: entry.license,
529
552
  pricingModel: entry.pricingModel,
530
553
  rationale: opts.rationale(entry),
531
- priority: isFirst ? "critical" : opts.priority,
554
+ priority,
532
555
  ciSnippet: generateCiSnippet(name, opts.ctx.ciProvider)
533
556
  });
534
557
  }
@@ -539,7 +562,7 @@ function collectLanguageRecs(langMap, recs, ctx) {
539
562
  names: langMap.sast,
540
563
  category: "sast",
541
564
  rationale: (e) => `${e.displayName} provides SAST for ${lang}`,
542
- priority: "recommended",
565
+ priority: "critical",
543
566
  ctx
544
567
  });
545
568
  collectCategoryRecs(recs, {
@@ -698,4 +721,4 @@ export {
698
721
  generateSecurityPlan,
699
722
  buildPlanFromAnalysis
700
723
  };
701
- //# sourceMappingURL=chunk-G2CSKBY5.js.map
724
+ //# sourceMappingURL=chunk-FJWWSVWB.js.map