sinapse-ai 1.6.1 → 1.8.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/.claude/CLAUDE.md +5 -11
  2. package/.claude/hooks/README.md +14 -1
  3. package/.claude/hooks/code-intel-pretool.cjs +115 -0
  4. package/.claude/hooks/enforce-delegation.cjs +31 -3
  5. package/.claude/hooks/enforce-framework-boundary.cjs +324 -0
  6. package/.claude/hooks/enforce-permission-mode.cjs +249 -0
  7. package/.claude/hooks/secret-scanning.cjs +34 -43
  8. package/.claude/hooks/synapse-engine.cjs +23 -23
  9. package/.claude/hooks/telemetry-post-tool.cjs +128 -0
  10. package/.claude/hooks/telemetry-stop.cjs +132 -0
  11. package/.claude/hooks/verify-packages.cjs +9 -2
  12. package/.claude/rules/documentation-first.md +1 -1
  13. package/.claude/rules/hook-governance.md +2 -0
  14. package/.sinapse-ai/cli/commands/health/index.js +24 -0
  15. package/.sinapse-ai/core/README.md +11 -0
  16. package/.sinapse-ai/core/config/config-loader.js +19 -0
  17. package/.sinapse-ai/core/config/merge-utils.js +8 -0
  18. package/.sinapse-ai/core/errors/constants.js +147 -0
  19. package/.sinapse-ai/core/errors/error-registry.js +176 -0
  20. package/.sinapse-ai/core/errors/index.js +50 -0
  21. package/.sinapse-ai/core/errors/serializer.js +147 -0
  22. package/.sinapse-ai/core/errors/sinapse-error.js +144 -0
  23. package/.sinapse-ai/core/errors/utils.js +187 -0
  24. package/.sinapse-ai/core/execution/build-orchestrator.js +47 -49
  25. package/.sinapse-ai/core/execution/build-state-manager.js +183 -31
  26. package/.sinapse-ai/core/execution/parallel-executor.js +7 -1
  27. package/.sinapse-ai/core/execution/semantic-merge-engine.js +26 -14
  28. package/.sinapse-ai/core/execution/subagent-dispatcher.js +201 -60
  29. package/.sinapse-ai/core/execution/wave-executor.js +4 -1
  30. package/.sinapse-ai/core/grounding/README.md +71 -11
  31. package/.sinapse-ai/core/health-check/checks/project/framework-config.js +38 -2
  32. package/.sinapse-ai/core/health-check/checks/project/package-json.js +47 -3
  33. package/.sinapse-ai/core/health-check/checks/services/gemini-cli.js +117 -0
  34. package/.sinapse-ai/core/health-check/checks/services/index.js +2 -0
  35. package/.sinapse-ai/core/health-check/healers/index.js +40 -3
  36. package/.sinapse-ai/core/ideation/ideation-engine.js +212 -107
  37. package/.sinapse-ai/core/ids/gate-evaluator.js +318 -0
  38. package/.sinapse-ai/core/ids/gates/g5-semantic-handshake.js +190 -0
  39. package/.sinapse-ai/core/ids/gates/g6-ci-integrity.js +162 -0
  40. package/.sinapse-ai/core/ids/index.js +30 -0
  41. package/.sinapse-ai/core/memory/__tests__/active-modules.verify.js +11 -0
  42. package/.sinapse-ai/core/memory/gotchas-memory.js +37 -2
  43. package/.sinapse-ai/core/orchestration/agent-invoker.js +29 -6
  44. package/.sinapse-ai/core/orchestration/brownfield-handler.js +36 -3
  45. package/.sinapse-ai/core/orchestration/condition-evaluator.js +57 -0
  46. package/.sinapse-ai/core/orchestration/executors/epic-3-executor.js +76 -5
  47. package/.sinapse-ai/core/orchestration/executors/epic-4-executor.js +63 -17
  48. package/.sinapse-ai/core/orchestration/executors/epic-6-executor.js +153 -41
  49. package/.sinapse-ai/core/orchestration/executors/epic-executor.js +40 -0
  50. package/.sinapse-ai/core/orchestration/greenfield-handler.js +87 -3
  51. package/.sinapse-ai/core/orchestration/master-orchestrator.js +150 -10
  52. package/.sinapse-ai/core/orchestration/parallel-executor.js +6 -1
  53. package/.sinapse-ai/core/orchestration/recovery-handler.js +81 -8
  54. package/.sinapse-ai/core/orchestration/workflow-executor.js +41 -0
  55. package/.sinapse-ai/core/registry/registry-loader.js +71 -5
  56. package/.sinapse-ai/core/registry/squad-agent-resolver.js +253 -0
  57. package/.sinapse-ai/core/synapse/context/context-tracker.js +104 -9
  58. package/.sinapse-ai/core/synapse/context/index.js +19 -0
  59. package/.sinapse-ai/core/synapse/context/semantic-handshake-engine.js +555 -0
  60. package/.sinapse-ai/core/synapse/diagnostics/collectors/pipeline-collector.js +4 -2
  61. package/.sinapse-ai/core/synapse/engine.js +43 -3
  62. package/.sinapse-ai/core/telemetry/ids-sink.js +188 -0
  63. package/.sinapse-ai/core/utils/output-formatter.js +8 -290
  64. package/.sinapse-ai/core/utils/spawn-safe.js +186 -0
  65. package/.sinapse-ai/core-config.yaml +68 -1
  66. package/.sinapse-ai/data/entity-registry.yaml +15082 -13618
  67. package/.sinapse-ai/data/registry-update-log.jsonl +143 -0
  68. package/.sinapse-ai/development/agents/developer.md +2 -0
  69. package/.sinapse-ai/development/agents/devops.md +9 -0
  70. package/.sinapse-ai/development/external-executors/README.md +18 -0
  71. package/.sinapse-ai/development/external-executors/codex.md +56 -0
  72. package/.sinapse-ai/development/scripts/populate-entity-registry.js +65 -9
  73. package/.sinapse-ai/development/scripts/squad/squad-downloader.js +169 -14
  74. package/.sinapse-ai/development/tasks/delegate-to-external-executor.md +152 -0
  75. package/.sinapse-ai/development/tasks/github-devops-pre-push-quality-gate.md +46 -29
  76. package/.sinapse-ai/development/tasks/update-sinapse.md +3 -3
  77. package/.sinapse-ai/hooks/sinapse-brand-grounding.cjs +4 -7
  78. package/.sinapse-ai/hooks/sinapse-ds-grounding.cjs +5 -8
  79. package/.sinapse-ai/hooks/sinapse-vault-grounding.cjs +6 -9
  80. package/.sinapse-ai/infrastructure/integrations/ai-providers/ai-provider-factory.js +4 -1
  81. package/.sinapse-ai/infrastructure/integrations/ai-providers/claude-provider.js +57 -55
  82. package/.sinapse-ai/infrastructure/integrations/pm-adapters/github-adapter.js +9 -7
  83. package/.sinapse-ai/infrastructure/scripts/ide-sync/gemini-commands.js +298 -0
  84. package/.sinapse-ai/infrastructure/scripts/ide-sync/index.js +127 -6
  85. package/.sinapse-ai/infrastructure/scripts/ide-sync/persona-renderer.js +97 -0
  86. package/.sinapse-ai/infrastructure/scripts/ide-sync/transformers/antigravity.js +121 -0
  87. package/.sinapse-ai/infrastructure/scripts/ide-sync/transformers/cursor.js +119 -0
  88. package/.sinapse-ai/infrastructure/scripts/ide-sync/transformers/github-copilot.js +191 -0
  89. package/.sinapse-ai/infrastructure/scripts/ide-sync/transformers/kimi.js +448 -0
  90. package/.sinapse-ai/install-manifest.yaml +218 -114
  91. package/.sinapse-ai/product/templates/engine/renderer.js +20 -1
  92. package/.sinapse-ai/scripts/pm.sh +18 -6
  93. package/bin/cli.js +17 -0
  94. package/bin/commands/agents.js +96 -0
  95. package/bin/commands/doctor.js +15 -0
  96. package/bin/commands/ideate.js +129 -0
  97. package/bin/commands/uninstall.js +40 -0
  98. package/bin/postinstall.js +50 -4
  99. package/bin/sinapse.js +146 -2
  100. package/bin/utils/secret-scanner-core.js +253 -0
  101. package/bin/utils/staged-secret-scan.js +106 -40
  102. package/docs/framework/collaboration-autonomy-plan.md +18 -18
  103. package/docs/guides/parallel-workflow.md +6 -6
  104. package/package.json +22 -5
  105. package/packages/installer/src/installer/git-hooks-installer.js +384 -0
  106. package/packages/installer/src/installer/sinapse-ai-installer.js +16 -0
  107. package/packages/installer/src/wizard/ide-config-generator.js +23 -0
  108. package/packages/installer/src/wizard/validators.js +38 -1
  109. package/packages/installer/tests/unit/artifact-copy-pipeline/artifact-copy-pipeline.test.js +5 -1
  110. package/packages/installer/tests/unit/doctor/doctor-checks.test.js +44 -22
  111. package/packages/installer/tests/unit/git-hooks-installer.test.js +262 -0
  112. package/scripts/eval-runner.js +422 -0
  113. package/scripts/generate-install-manifest.js +13 -9
  114. package/scripts/generate-synapse-runtime.js +51 -0
  115. package/scripts/regenerate-orqx-stubs.ps1 +6 -5
  116. package/scripts/validate-all.js +1 -0
  117. package/scripts/validate-evals.js +466 -0
  118. package/scripts/validate-schemas.js +539 -0
  119. package/scripts/validate-squad-orqx.js +9 -2
  120. package/squads/claude-code-mastery/knowledge-base/memory-systems-reference.md +1 -1
  121. package/squads/squad-brand/templates/client-delivery-template.md +1 -1
  122. package/squads/squad-content/knowledge-base/social-compression-framework.md +1 -1
  123. package/squads/squad-council/knowledge-base/brand-strategy-models.md +1 -1
  124. package/.sinapse-ai/development/scripts/elicitation-engine.js +0 -385
  125. package/.sinapse-ai/development/scripts/elicitation-session-manager.js +0 -300
  126. package/.sinapse-ai/development/tasks/test-validation-task.md +0 -172
  127. package/docs/chrome-brain-upgrade-plan.md +0 -624
  128. package/docs/constitution-compliance.md +0 -87
  129. package/docs/mega-upgrade-orchestration-plan.md +0 -71
  130. package/docs/research-synthesis-for-upgrade.md +0 -511
  131. package/docs/security-audit-report.md +0 -306
@@ -0,0 +1,253 @@
1
+ /**
2
+ * Squad Agent Resolver
3
+ *
4
+ * epic: orchestration-consolidation, F2 — makes the 177 squad personas (plus the
5
+ * framework agents) addressable BY CODE. Before this, the SubagentDispatcher knew
6
+ * only ~10 generic agents (@dev/@qa/...) and built a one-line "You are @x" prompt,
7
+ * discarding the real persona. This indexes every agent .md across `squads/` and
8
+ * the framework agent dirs, and loads the FULL persona on demand.
9
+ *
10
+ * Convention (verified across all squads): the agent id IS the file basename in
11
+ * kebab-case (e.g. `penetration-tester.md` -> id `penetration-tester`), which
12
+ * matches the embedded `id:` field. We key by basename for robustness and also
13
+ * register the embedded id / display name as aliases when present.
14
+ *
15
+ * @module core/registry/squad-agent-resolver
16
+ * @version 1.0.0
17
+ */
18
+
19
+ const fs = require('fs');
20
+ const path = require('path');
21
+
22
+ /** Directories scanned for agent definitions, relative to projectRoot. */
23
+ const AGENT_DIRS = [
24
+ 'squads', // squads/<squad>/agents/<id>.md
25
+ path.join('.sinapse-ai', 'agents'),
26
+ path.join('.sinapse-ai', 'development', 'agents'),
27
+ ];
28
+
29
+ /**
30
+ * Short-form aliases used across the framework (dispatcher agentMapping, CLAUDE.md
31
+ * command table) → canonical file id. Lets '@dev' resolve to the developer persona
32
+ * instead of falling through to a one-line generic prompt.
33
+ */
34
+ const ALIASES = {
35
+ dev: 'developer',
36
+ qa: 'quality-gate',
37
+ pm: 'project-lead',
38
+ po: 'product-lead',
39
+ sm: 'sprint-lead',
40
+ ux: 'ux-design-expert',
41
+ 'ux-expert': 'ux-design-expert',
42
+ };
43
+
44
+ class SquadAgentResolver {
45
+ /**
46
+ * @param {string} projectRoot - Project root directory
47
+ */
48
+ constructor(projectRoot = process.cwd()) {
49
+ this.projectRoot = projectRoot;
50
+ this._index = null; // Map<normalizedKey, entry> — built lazily
51
+ this._personaCache = new Map(); // id -> persona content
52
+ }
53
+
54
+ /**
55
+ * Normalize an agent reference for lookup: strip leading '@', lowercase,
56
+ * collapse separators. So '@penetration-tester', 'penetration_tester' and
57
+ * 'Penetration Tester' all resolve to the same key.
58
+ * @param {string} ref
59
+ * @returns {string}
60
+ */
61
+ static normalizeKey(ref) {
62
+ return String(ref || '')
63
+ .replace(/^@/, '')
64
+ .trim()
65
+ .toLowerCase()
66
+ .replace(/[\s_]+/g, '-');
67
+ }
68
+
69
+ /**
70
+ * Build (once) the index of every agent definition on disk.
71
+ * @returns {Map<string, Object>} normalizedKey -> { id, name, squad, filePath }
72
+ */
73
+ buildIndex() {
74
+ if (this._index) return this._index;
75
+ const index = new Map();
76
+
77
+ const addEntry = (filePath, squad) => {
78
+ const base = path.basename(filePath, '.md');
79
+ if (!base || base.toUpperCase() === 'README') return;
80
+ const key = SquadAgentResolver.normalizeKey(base);
81
+ // First writer wins for a given key, but squad agents take precedence over
82
+ // framework duplicates only if a squad entry isn't already registered.
83
+ if (!index.has(key)) {
84
+ index.set(key, { id: base, name: base, squad, filePath });
85
+ }
86
+ };
87
+
88
+ for (const rel of AGENT_DIRS) {
89
+ const dir = path.join(this.projectRoot, rel);
90
+ if (!fs.existsSync(dir)) continue;
91
+
92
+ if (rel === 'squads') {
93
+ // squads/<squad>/agents/*.md
94
+ let squadDirs = [];
95
+ try {
96
+ squadDirs = fs.readdirSync(dir, { withFileTypes: true });
97
+ } catch {
98
+ squadDirs = [];
99
+ }
100
+ for (const sd of squadDirs) {
101
+ if (!sd.isDirectory()) continue;
102
+ const agentsDir = path.join(dir, sd.name, 'agents');
103
+ if (!fs.existsSync(agentsDir)) continue;
104
+ for (const f of this._listMd(agentsDir)) {
105
+ addEntry(path.join(agentsDir, f), sd.name);
106
+ }
107
+ }
108
+ } else {
109
+ // flat dir of *.md
110
+ for (const f of this._listMd(dir)) {
111
+ addEntry(path.join(dir, f), null);
112
+ }
113
+ }
114
+ }
115
+
116
+ this._index = index;
117
+ return index;
118
+ }
119
+
120
+ /**
121
+ * List .md files in a directory (non-recursive), tolerant of read errors.
122
+ * @param {string} dir
123
+ * @returns {string[]}
124
+ * @private
125
+ */
126
+ _listMd(dir) {
127
+ try {
128
+ return fs.readdirSync(dir).filter((f) => f.toLowerCase().endsWith('.md'));
129
+ } catch {
130
+ return [];
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Resolve an agent reference to its index entry.
136
+ * @param {string} ref - e.g. '@penetration-tester', 'dev', 'cloud-security-engineer'
137
+ * @returns {Object|null} { id, name, squad, filePath } or null when unknown
138
+ */
139
+ resolve(ref) {
140
+ const index = this.buildIndex();
141
+ const key = SquadAgentResolver.normalizeKey(ref);
142
+ return index.get(key) || index.get(ALIASES[key]) || null;
143
+ }
144
+
145
+ /**
146
+ * True when the reference maps to a known agent definition on disk.
147
+ * @param {string} ref
148
+ * @returns {boolean}
149
+ */
150
+ has(ref) {
151
+ return this.resolve(ref) !== null;
152
+ }
153
+
154
+ /**
155
+ * Load the full persona markdown for an agent reference.
156
+ * @param {string} ref
157
+ * @returns {string|null} persona content, or null when unknown/unreadable
158
+ */
159
+ loadPersona(ref) {
160
+ const entry = this.resolve(ref);
161
+ if (!entry) return null;
162
+ if (this._personaCache.has(entry.id)) return this._personaCache.get(entry.id);
163
+ let content = null;
164
+ try {
165
+ content = fs.readFileSync(entry.filePath, 'utf8');
166
+ } catch {
167
+ content = null;
168
+ }
169
+ this._personaCache.set(entry.id, content);
170
+ return content;
171
+ }
172
+
173
+ /**
174
+ * All known agent ids (sorted), for diagnostics / `sinapse` listing.
175
+ * @returns {string[]}
176
+ */
177
+ listIds() {
178
+ return [...this.buildIndex().values()].map((e) => e.id).sort();
179
+ }
180
+
181
+ /**
182
+ * Count of indexed agents.
183
+ * @returns {number}
184
+ */
185
+ size() {
186
+ return this.buildIndex().size;
187
+ }
188
+
189
+ /**
190
+ * Normalized metadata for an agent, extracted from whatever structure the
191
+ * file uses (frontmatter, `# Agent: X`, `# X`, or an embedded ```yaml block).
192
+ * This is the uniform schema (SCHEMA-001 closure) without mutating the 199
193
+ * heterogeneous persona files — the canonical shape is DERIVED, not imposed.
194
+ *
195
+ * @param {string} ref
196
+ * @returns {{id, name, squad, type, hasStructuredYaml, file}|null}
197
+ */
198
+ describe(ref) {
199
+ const entry = this.resolve(ref);
200
+ if (!entry) return null;
201
+ const content = this.loadPersona(entry.id) || '';
202
+ return {
203
+ id: entry.id,
204
+ name: SquadAgentResolver.extractName(content, entry.id),
205
+ squad: entry.squad || 'framework',
206
+ type: /-orqx$/.test(entry.id) ? 'orchestrator' : 'specialist',
207
+ hasStructuredYaml: /```ya?ml/.test(content) || /^\s*agent:\s*$/m.test(content),
208
+ file: entry.filePath,
209
+ };
210
+ }
211
+
212
+ /**
213
+ * Describe every indexed agent (sorted by id) — the uniform registry view.
214
+ * @returns {Array<Object>}
215
+ */
216
+ list() {
217
+ return this.listIds().map((id) => this.describe(id));
218
+ }
219
+
220
+ /**
221
+ * Best-effort display name from heterogeneous structures. Falls back to a
222
+ * title-cased id so the result is never empty.
223
+ * @param {string} content - File content
224
+ * @param {string} id - Canonical id (filename)
225
+ * @returns {string}
226
+ */
227
+ static extractName(content, id) {
228
+ const src = String(content || '');
229
+ // 1. YAML frontmatter `name:`
230
+ const fm = src.match(/^---\s*\n([\s\S]*?)\n---/);
231
+ if (fm) {
232
+ const m = fm[1].match(/^\s*name:\s*["']?([^"'\n]+)["']?\s*$/m);
233
+ if (m && m[1].trim()) return m[1].trim();
234
+ }
235
+ // 2. Embedded yaml `name:` (inside an ```yaml block or agent: section)
236
+ const ym = src.match(/^\s*name:\s*["']?([^"'\n]+)["']?\s*$/m);
237
+ if (ym && ym[1].trim()) return ym[1].trim();
238
+ // 3. `# Agent: Name`
239
+ const ah = src.match(/^#\s*Agent:\s*(.+?)\s*$/m);
240
+ if (ah && ah[1].trim()) return ah[1].trim();
241
+ // 4. First `# Heading`
242
+ const h1 = src.match(/^#\s+(.+?)\s*$/m);
243
+ if (h1 && h1[1].trim()) return h1[1].trim().replace(/^@/, '');
244
+ // 5. Title-cased id
245
+ return String(id)
246
+ .split('-')
247
+ .map((w) => (w ? w[0].toUpperCase() + w.slice(1) : w))
248
+ .join(' ');
249
+ }
250
+ }
251
+
252
+ module.exports = SquadAgentResolver;
253
+ module.exports.SquadAgentResolver = SquadAgentResolver;
@@ -5,13 +5,16 @@
5
5
  * based on estimated token usage. Provides token budgets and layer filtering
6
6
  * per bracket for the SynapseEngine orchestrator.
7
7
  *
8
- * Pure arithmetic module zero I/O, zero external dependencies.
8
+ * Reads model context window from core-config.yaml models.registry.
9
9
  *
10
10
  * @module core/synapse/context/context-tracker
11
- * @version 1.0.0
11
+ * @version 1.1.0
12
12
  * @created Story SYN-3 - Context Bracket Tracker
13
13
  */
14
14
 
15
+ const fs = require('fs');
16
+ const path = require('path');
17
+
15
18
  /**
16
19
  * Bracket definitions with thresholds and token budgets.
17
20
  *
@@ -51,11 +54,84 @@ const XML_SAFETY_MULTIPLIER = 1.2;
51
54
 
52
55
  /**
53
56
  * Default configuration values.
57
+ * maxContext is the fallback when core-config.yaml is unavailable.
54
58
  */
55
- const DEFAULTS = {
59
+ const DEFAULTS = Object.freeze({
56
60
  avgTokensPerPrompt: 1500,
57
61
  maxContext: 200000,
58
- };
62
+ });
63
+
64
+ /** Cache for model config by project root (read once per root per process). */
65
+ const _modelConfigCache = new Map();
66
+
67
+ function cloneModelConfig(config) {
68
+ return { ...config };
69
+ }
70
+
71
+ function cacheModelConfig(root, config) {
72
+ const cachedConfig = Object.freeze(cloneModelConfig(config));
73
+ _modelConfigCache.set(root, cachedConfig);
74
+ return cloneModelConfig(cachedConfig);
75
+ }
76
+
77
+ function isPositiveFiniteNumber(value) {
78
+ return Number.isFinite(value) && value > 0;
79
+ }
80
+
81
+ /**
82
+ * Resolve the project root used for model config lookup.
83
+ *
84
+ * @param {string|null} basePath - Optional project root override
85
+ * @returns {string}
86
+ */
87
+ function resolveConfigRoot(basePath) {
88
+ return path.resolve(basePath || path.resolve(__dirname, '..', '..', '..', '..'));
89
+ }
90
+
91
+ /**
92
+ * Read model configuration from core-config.yaml → models section.
93
+ * Returns { contextWindow, avgTokensPerPrompt } for the active model.
94
+ * Falls back to DEFAULTS if config is missing or malformed.
95
+ *
96
+ * @param {string|null} [basePath=null] - Project root override (defaults to __dirname-based resolution)
97
+ * @returns {{ maxContext: number, avgTokensPerPrompt: number }}
98
+ */
99
+ function getModelConfig(basePath = null) {
100
+ const root = resolveConfigRoot(basePath);
101
+ if (_modelConfigCache.has(root)) return cloneModelConfig(_modelConfigCache.get(root));
102
+
103
+ try {
104
+ const yaml = require('js-yaml');
105
+ const configPath = path.join(root, '.sinapse-ai', 'core-config.yaml');
106
+ if (!fs.existsSync(configPath)) {
107
+ return cacheModelConfig(root, DEFAULTS);
108
+ }
109
+
110
+ const config = yaml.load(fs.readFileSync(configPath, 'utf8'));
111
+ const models = config && config.models;
112
+ if (!models || !models.registry || !models.active) {
113
+ return cacheModelConfig(root, DEFAULTS);
114
+ }
115
+
116
+ const activeModel = models.registry[models.active];
117
+ if (!activeModel || !isPositiveFiniteNumber(activeModel.contextWindow)) {
118
+ return cacheModelConfig(root, DEFAULTS);
119
+ }
120
+
121
+ const modelConfig = {
122
+ maxContext: activeModel.contextWindow,
123
+ avgTokensPerPrompt: isPositiveFiniteNumber(activeModel.avgTokensPerPrompt)
124
+ ? activeModel.avgTokensPerPrompt
125
+ : DEFAULTS.avgTokensPerPrompt,
126
+ };
127
+ return cacheModelConfig(root, modelConfig);
128
+ } catch (err) {
129
+ if (process.env.DEBUG || process.env.SINAPSE_DEBUG) {
130
+ console.warn('[context-tracker] Failed to load model config, using defaults:', err.message);
131
+ }
132
+ return cacheModelConfig(root, DEFAULTS);
133
+ }
134
+ }
59
135
 
60
136
  /**
61
137
  * Layer configurations per bracket.
@@ -101,16 +177,20 @@ function calculateBracket(contextPercent) {
101
177
  * Formula: 100 - ((promptCount * avgTokensPerPrompt) / maxContext * 100)
102
178
  * Result is clamped to 0-100 range.
103
179
  *
180
+ * Reads maxContext and avgTokensPerPrompt from core-config.yaml → models.registry
181
+ * for the active model. Options parameter can override for testing.
182
+ *
104
183
  * @param {number} promptCount - Number of prompts in current session
105
- * @param {Object} [options={}] - Configuration options
106
- * @param {number} [options.avgTokensPerPrompt=1500] - Average tokens per prompt
107
- * @param {number} [options.maxContext=200000] - Maximum context window size in tokens
184
+ * @param {Object} [options={}] - Configuration options (override config values)
185
+ * @param {number} [options.avgTokensPerPrompt] - Average tokens per prompt
186
+ * @param {number} [options.maxContext] - Maximum context window size in tokens
108
187
  * @returns {number} Percentage of context remaining (0.0 to 100.0)
109
188
  */
110
189
  function estimateContextPercent(promptCount, options = {}) {
190
+ const modelConfig = getModelConfig();
111
191
  const {
112
- avgTokensPerPrompt = DEFAULTS.avgTokensPerPrompt,
113
- maxContext = DEFAULTS.maxContext,
192
+ avgTokensPerPrompt = modelConfig.avgTokensPerPrompt,
193
+ maxContext = modelConfig.maxContext,
114
194
  } = options;
115
195
 
116
196
  if (typeof promptCount !== 'number' || isNaN(promptCount) || promptCount < 0) {
@@ -184,6 +264,19 @@ function needsMemoryHints(bracket) {
184
264
  return bracket === 'DEPLETED' || bracket === 'CRITICAL';
185
265
  }
186
266
 
267
+ /**
268
+ * Reset the model config cache. Useful for tests or after config changes.
269
+ *
270
+ * @param {string|null} [basePath=null] - Optional project root override
271
+ */
272
+ function resetModelConfigCache(basePath = null) {
273
+ if (basePath === null) {
274
+ _modelConfigCache.clear();
275
+ return;
276
+ }
277
+ _modelConfigCache.delete(resolveConfigRoot(basePath));
278
+ }
279
+
187
280
  module.exports = {
188
281
  calculateBracket,
189
282
  estimateContextPercent,
@@ -191,6 +284,8 @@ module.exports = {
191
284
  getActiveLayers,
192
285
  needsHandoffWarning,
193
286
  needsMemoryHints,
287
+ getModelConfig,
288
+ resetModelConfigCache,
194
289
  BRACKETS,
195
290
  TOKEN_BUDGETS,
196
291
  DEFAULTS,
@@ -0,0 +1,19 @@
1
+ /**
2
+ * SYNAPSE context runtime exports.
3
+ *
4
+ * @module core/synapse/context
5
+ */
6
+
7
+ 'use strict';
8
+
9
+ const path = require('path');
10
+
11
+ const contextTracker = require(path.resolve(__dirname, './context-tracker.js'));
12
+ const contextBuilder = require(path.resolve(__dirname, './context-builder.js'));
13
+ const semanticHandshake = require(path.resolve(__dirname, './semantic-handshake-engine.js'));
14
+
15
+ module.exports = {
16
+ ...contextTracker,
17
+ ...contextBuilder,
18
+ ...semanticHandshake,
19
+ };