aios-core 4.0.0 → 4.0.2

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 (30) hide show
  1. package/.aios-core/cli/commands/pro/index.js +82 -148
  2. package/.aios-core/core/synapse/domain/domain-loader.js +2 -2
  3. package/.aios-core/core/synapse/engine.js +17 -4
  4. package/.aios-core/core/synapse/memory/memory-bridge.js +246 -0
  5. package/.aios-core/core/synapse/output/formatter.js +34 -12
  6. package/.aios-core/core/synapse/scripts/generate-constitution.js +204 -0
  7. package/.aios-core/core/synapse/utils/tokens.js +25 -0
  8. package/.aios-core/data/aios-kb.md +2 -4
  9. package/.aios-core/data/entity-registry.yaml +61 -8
  10. package/.aios-core/development/scripts/unified-activation-pipeline.js +9 -1
  11. package/.aios-core/framework-config.yaml +1 -1
  12. package/.aios-core/install-manifest.yaml +33 -21
  13. package/.aios-core/lib/build.json +1 -0
  14. package/.aios-core/package.json +2 -1
  15. package/.aios-core/user-guide.md +1 -1
  16. package/.claude/CLAUDE.md +8 -9
  17. package/.claude/hooks/README.md +169 -0
  18. package/.claude/hooks/precompact-session-digest.js +46 -0
  19. package/.claude/hooks/synapse-engine.js +87 -0
  20. package/bin/aios-init.js +4 -4
  21. package/bin/aios-minimal.js +1 -4
  22. package/bin/aios.js +1 -1
  23. package/bin/modules/env-config.js +0 -1
  24. package/package.json +4 -1
  25. package/packages/aios-pro-cli/bin/aios-pro.js +158 -0
  26. package/packages/aios-pro-cli/package.json +32 -0
  27. package/packages/installer/package.json +1 -1
  28. package/packages/installer/src/installer/aios-core-installer.js +23 -0
  29. package/packages/installer/src/wizard/ide-config-generator.js +146 -1
  30. package/packages/installer/src/wizard/index.js +49 -32
@@ -23,8 +23,40 @@ const path = require('path');
23
23
  const fs = require('fs');
24
24
  const readline = require('readline');
25
25
 
26
- // Resolve license modules (relative from .aios-core/cli/commands/pro/)
27
- const licensePath = path.resolve(__dirname, '..', '..', '..', '..', 'pro', 'license');
26
+ // BUG-6 fix (INS-1): Dynamic licensePath resolution
27
+ // In framework-dev: __dirname = aios-core/.aios-core/cli/commands/pro ../../../../pro/license
28
+ // In project-dev: pro is installed via npm as @aios-fullstack/pro
29
+ function resolveLicensePath() {
30
+ // 1. Try relative path (framework-dev mode)
31
+ const relativePath = path.resolve(__dirname, '..', '..', '..', '..', 'pro', 'license');
32
+ if (fs.existsSync(relativePath)) {
33
+ return relativePath;
34
+ }
35
+
36
+ // 2. Try node_modules/@aios-fullstack/pro/license (project-dev mode)
37
+ try {
38
+ const proPkg = require.resolve('@aios-fullstack/pro/package.json');
39
+ const proDir = path.dirname(proPkg);
40
+ const npmPath = path.join(proDir, 'license');
41
+ if (fs.existsSync(npmPath)) {
42
+ return npmPath;
43
+ }
44
+ } catch {
45
+ // @aios-fullstack/pro not installed via npm
46
+ }
47
+
48
+ // 3. Try project root node_modules (fallback)
49
+ const projectRoot = process.cwd();
50
+ const cwdPath = path.join(projectRoot, 'node_modules', '@aios-fullstack', 'pro', 'license');
51
+ if (fs.existsSync(cwdPath)) {
52
+ return cwdPath;
53
+ }
54
+
55
+ // Return relative path as default (will fail gracefully in loadLicenseModules)
56
+ return relativePath;
57
+ }
58
+
59
+ const licensePath = resolveLicensePath();
28
60
 
29
61
  /**
30
62
  * Lazy-load license modules (avoids failing if pro module not installed)
@@ -65,7 +97,7 @@ function loadLicenseModules() {
65
97
  };
66
98
  } catch (error) {
67
99
  console.error('AIOS Pro license module not available.');
68
- console.error('Install AIOS Pro: npm install @aios/pro');
100
+ console.error('Install AIOS Pro: npm install @aios-fullstack/pro');
69
101
  process.exit(1);
70
102
  }
71
103
  }
@@ -487,172 +519,75 @@ async function validateAction() {
487
519
  // ---------------------------------------------------------------------------
488
520
 
489
521
  /**
490
- * Setup GitHub Packages access for @aios/pro installation.
522
+ * Setup and verify @aios-fullstack/pro installation.
491
523
  *
492
- * This command helps users configure their .npmrc to access
493
- * the private @aios scope on GitHub Packages.
524
+ * Since @aios-fullstack/pro is published on the public npm registry,
525
+ * no special token or .npmrc configuration is needed. This command
526
+ * installs the package and verifies it's working.
494
527
  *
495
528
  * @param {object} options - Command options
496
- * @param {string} options.token - GitHub Personal Access Token
497
- * @param {boolean} options.global - Configure globally vs project-level
529
+ * @param {boolean} options.verify - Only verify without installing
498
530
  */
499
531
  async function setupAction(options) {
500
- const os = require('os');
501
- const homedir = os.homedir();
502
-
503
- console.log('\nAIOS Pro - GitHub Packages Setup\n');
504
- console.log('This will configure npm to access @aios/pro from GitHub Packages.');
505
- console.log('');
532
+ console.log('\nAIOS Pro - Setup\n');
506
533
 
507
- // Determine .npmrc location
508
- const npmrcPath = options.global
509
- ? path.join(homedir, '.npmrc')
510
- : path.join(process.cwd(), '.npmrc');
534
+ if (options.verify) {
535
+ // Verify-only mode
536
+ console.log('Verifying @aios-fullstack/pro installation...\n');
511
537
 
512
- const scopeConfig = '@aios:registry=https://npm.pkg.github.com';
513
- const tokenConfig = '//npm.pkg.github.com/:_authToken=';
514
-
515
- // Check if token provided
516
- let token = options.token;
517
-
518
- if (!token) {
519
- console.log('To install @aios/pro, you need a GitHub Personal Access Token (PAT)');
520
- console.log('with the "read:packages" scope.');
521
- console.log('');
522
- console.log('Create one at: https://github.com/settings/tokens/new');
523
- console.log('');
524
- console.log('Required scopes:');
525
- console.log(' - read:packages (download packages from GitHub Packages)');
526
- console.log('');
527
-
528
- // Interactive token input
529
- const rl = readline.createInterface({
530
- input: process.stdin,
531
- output: process.stdout,
532
- });
533
-
534
- token = await new Promise((resolve) => {
535
- rl.question('Enter your GitHub PAT (or press Enter to skip): ', (answer) => {
536
- rl.close();
537
- resolve(answer.trim());
538
- });
539
- });
540
-
541
- if (!token) {
542
- console.log('\nSetup cancelled. You can run this command again with:');
543
- console.log(' aios pro setup --token <YOUR_GITHUB_PAT>');
544
- console.log('');
545
- console.log('Or manually add to your .npmrc:');
546
- console.log(` ${scopeConfig}`);
547
- console.log(` ${tokenConfig}<YOUR_GITHUB_PAT>`);
548
- return;
549
- }
550
- }
551
-
552
- // Validate token format (basic check)
553
- if (!token.startsWith('ghp_') && !token.startsWith('github_pat_')) {
554
- console.log('\n⚠️ Warning: Token does not appear to be a valid GitHub PAT.');
555
- console.log('Expected format: ghp_... or github_pat_...');
556
-
557
- const confirmed = await confirm('Continue anyway? (y/N): ');
558
- if (!confirmed) {
559
- console.log('Setup cancelled.');
560
- return;
561
- }
562
- }
563
-
564
- // Read existing .npmrc or create new
565
- let npmrcContent = '';
566
- try {
567
- npmrcContent = fs.readFileSync(npmrcPath, 'utf8');
568
- } catch {
569
- // File doesn't exist, will create new
570
- }
571
-
572
- // Check if already configured
573
- if (npmrcContent.includes('@aios:registry')) {
574
- console.log(`\n⚠️ @aios registry already configured in ${npmrcPath}`);
575
-
576
- const overwrite = await confirm('Overwrite existing configuration? (y/N): ');
577
- if (!overwrite) {
578
- console.log('Setup cancelled. Existing configuration preserved.');
579
- return;
580
- }
581
-
582
- // Remove existing @aios config
583
- npmrcContent = npmrcContent
584
- .split('\n')
585
- .filter((line) => !line.includes('@aios:') && !line.includes('npm.pkg.github.com/:_authToken'))
586
- .join('\n');
587
- }
588
-
589
- // Add new configuration
590
- const newConfig = [
591
- '',
592
- '# AIOS Pro - GitHub Packages (added by aios pro setup)',
593
- scopeConfig,
594
- `${tokenConfig}${token}`,
595
- '',
596
- ].join('\n');
597
-
598
- npmrcContent = npmrcContent.trimEnd() + newConfig;
599
-
600
- // Write .npmrc
601
- try {
602
- fs.writeFileSync(npmrcPath, npmrcContent, 'utf8');
603
- console.log(`\n✅ Configuration written to ${npmrcPath}`);
604
- } catch (error) {
605
- console.error(`\n❌ Failed to write ${npmrcPath}: ${error.message}`);
606
- console.log('\nManually add these lines to your .npmrc:');
607
- console.log(` ${scopeConfig}`);
608
- console.log(` ${tokenConfig}<YOUR_TOKEN>`);
609
- process.exit(1);
610
- }
611
-
612
- // Add .npmrc to .gitignore if project-level
613
- if (!options.global) {
614
- const gitignorePath = path.join(process.cwd(), '.gitignore');
615
538
  try {
616
- let gitignore = '';
617
- try {
618
- gitignore = fs.readFileSync(gitignorePath, 'utf8');
619
- } catch {
620
- // No .gitignore exists
621
- }
622
-
623
- if (!gitignore.includes('.npmrc')) {
624
- gitignore += '\n# AIOS Pro credentials (DO NOT COMMIT)\n.npmrc\n';
625
- fs.writeFileSync(gitignorePath, gitignore, 'utf8');
626
- console.log(' Added .npmrc to .gitignore');
539
+ const { execSync } = require('child_process');
540
+ const result = execSync('npm ls @aios-fullstack/pro --json', {
541
+ stdio: 'pipe',
542
+ timeout: 15000,
543
+ });
544
+ const parsed = JSON.parse(result.toString());
545
+ const deps = parsed.dependencies || {};
546
+ if (deps['@aios-fullstack/pro']) {
547
+ console.log(`✅ @aios-fullstack/pro@${deps['@aios-fullstack/pro'].version} is installed`);
548
+ } else {
549
+ console.log(' @aios-fullstack/pro is not installed');
550
+ console.log('');
551
+ console.log('Install with:');
552
+ console.log(' npm install @aios-fullstack/pro');
627
553
  }
628
554
  } catch {
629
- console.log('⚠️ Could not update .gitignore. Please add .npmrc manually.');
555
+ console.log(' @aios-fullstack/pro is not installed');
556
+ console.log('');
557
+ console.log('Install with:');
558
+ console.log(' npm install @aios-fullstack/pro');
630
559
  }
560
+ return;
631
561
  }
632
562
 
633
- // Verify access
634
- console.log('\nVerifying access to GitHub Packages...');
563
+ // Install mode
564
+ console.log('@aios-fullstack/pro is available on the public npm registry.');
565
+ console.log('No special tokens or configuration needed.\n');
566
+
567
+ console.log('Installing @aios-fullstack/pro...\n');
635
568
 
636
569
  try {
637
570
  const { execSync } = require('child_process');
638
- execSync('npm view @aios/pro --registry=https://npm.pkg.github.com', {
639
- stdio: 'pipe',
640
- timeout: 15000,
571
+ execSync('npm install @aios-fullstack/pro', {
572
+ stdio: 'inherit',
573
+ timeout: 120000,
641
574
  });
642
- console.log('✅ Access verified! You can now install @aios/pro');
643
- } catch {
644
- console.log('⚠️ Could not verify access (package may not be published yet)');
645
- console.log(' Configuration is saved. Try: npm install @aios/pro');
575
+ console.log('\n✅ @aios-fullstack/pro installed successfully!');
576
+ } catch (error) {
577
+ console.error(`\n❌ Installation failed: ${error.message}`);
578
+ console.log('\nTry manually:');
579
+ console.log(' npm install @aios-fullstack/pro');
580
+ process.exit(1);
646
581
  }
647
582
 
648
583
  console.log('\n--- Setup Complete ---');
649
584
  console.log('');
650
- console.log('To install AIOS Pro:');
651
- console.log(' npm install @aios/pro');
652
- console.log('');
653
585
  console.log('To activate your license:');
654
586
  console.log(' aios pro activate --key PRO-XXXX-XXXX-XXXX-XXXX');
655
587
  console.log('');
588
+ console.log('To check license status:');
589
+ console.log(' aios pro status');
590
+ console.log('');
656
591
  console.log('Documentation: https://synkra.ai/pro/docs');
657
592
  console.log('');
658
593
  }
@@ -704,9 +639,8 @@ function createProCommand() {
704
639
  // aios pro setup (AC-12: Install-gate)
705
640
  proCmd
706
641
  .command('setup')
707
- .description('Configure GitHub Packages access for @aios/pro')
708
- .option('-t, --token <token>', 'GitHub Personal Access Token')
709
- .option('-g, --global', 'Configure globally (~/.npmrc) instead of project-level')
642
+ .description('Install and verify @aios-fullstack/pro')
643
+ .option('--verify', 'Only verify installation without installing')
710
644
  .action(setupAction);
711
645
 
712
646
  return proCmd;
@@ -169,7 +169,7 @@ function loadDomainFile(domainPath) {
169
169
  // First pass: detect if file uses KEY=VALUE format
170
170
  for (const line of lines) {
171
171
  const trimmed = line.trim();
172
- if (trimmed && !trimmed.startsWith('#') && /^[A-Z_]+_RULE_\d+=/.test(trimmed)) {
172
+ if (trimmed && !trimmed.startsWith('#') && /^[A-Z][A-Z0-9_]*=/.test(trimmed)) {
173
173
  hasKeyValueFormat = true;
174
174
  break;
175
175
  }
@@ -185,7 +185,7 @@ function loadDomainFile(domainPath) {
185
185
 
186
186
  if (hasKeyValueFormat) {
187
187
  // KEY=VALUE format: extract value from DOMAIN_RULE_N=text
188
- const match = trimmed.match(/^[A-Z_]+_RULE_\d+=(.+)$/);
188
+ const match = trimmed.match(/^[A-Z][A-Z0-9_]*=(.+)$/);
189
189
  if (match) {
190
190
  rules.push(match[1].trim());
191
191
  }
@@ -20,6 +20,7 @@ const {
20
20
  } = require('./context/context-tracker');
21
21
 
22
22
  const { formatSynapseRules } = require('./output/formatter');
23
+ const { MemoryBridge } = require('./memory/memory-bridge');
23
24
 
24
25
  // ---------------------------------------------------------------------------
25
26
  // Layer Imports (graceful — layers from SYN-4/SYN-5 may not exist yet)
@@ -186,6 +187,9 @@ class SynapseEngine {
186
187
  /** @type {Array<import('./layers/layer-processor')>} */
187
188
  this.layers = [];
188
189
 
190
+ /** @type {MemoryBridge} Feature-gated MIS consumer (SYN-10) */
191
+ this.memoryBridge = new MemoryBridge();
192
+
189
193
  for (const mod of LAYER_MODULES) {
190
194
  const LayerClass = loadLayerModule(mod.path);
191
195
  if (LayerClass) {
@@ -211,9 +215,9 @@ class SynapseEngine {
211
215
  * @param {object} session - Session state (SYN-2 schema)
212
216
  * @param {number} [session.prompt_count=0] - Number of prompts so far
213
217
  * @param {object} [processConfig] - Per-call config overrides
214
- * @returns {{ xml: string, metrics: object }}
218
+ * @returns {Promise<{ xml: string, metrics: object }>}
215
219
  */
216
- process(prompt, session, processConfig) {
220
+ async process(prompt, session, processConfig) {
217
221
  const safeProcessConfig = (processConfig && typeof processConfig === 'object') ? processConfig : {};
218
222
  const mergedConfig = { ...this.config, ...safeProcessConfig };
219
223
  const metrics = new PipelineMetrics();
@@ -283,9 +287,18 @@ class SynapseEngine {
283
287
  }
284
288
  }
285
289
 
286
- // 3. Memory bridge placeholders (SYN-10 future no-op)
290
+ // 3. Memory bridge (SYN-10)feature-gated MIS consumer
287
291
  if (needsMemoryHints(bracket)) {
288
- // Placeholder: SYN-10 will inject memory hints here
292
+ const hints = await this.memoryBridge.getMemoryHints(
293
+ (session && session.activeAgent) || (session && session.active_agent) || '',
294
+ bracket,
295
+ tokenBudget,
296
+ );
297
+ if (hints.length > 0) {
298
+ const memoryResult = { layer: 'memory', rules: hints, metadata: { layer: 'memory', source: 'memory' } };
299
+ results.push(memoryResult);
300
+ previousLayers.push(memoryResult);
301
+ }
289
302
  }
290
303
 
291
304
  metrics.totalEnd = Date.now();
@@ -0,0 +1,246 @@
1
+ /**
2
+ * Memory Bridge — Feature-gated MIS consumer for SYNAPSE engine.
3
+ *
4
+ * Connects SynapseEngine to the Memory Intelligence System (MIS)
5
+ * via MemoryLoader API. Implements bracket-aware retrieval with
6
+ * agent-scoped sector filtering and token budget enforcement.
7
+ *
8
+ * Consumer-only: reads from MIS APIs, never modifies memory stores.
9
+ * Graceful no-op when pro feature is unavailable.
10
+ *
11
+ * @module core/synapse/memory/memory-bridge
12
+ * @version 1.0.0
13
+ * @created Story SYN-10 - Pro Memory Bridge (Feature-Gated MIS Consumer)
14
+ */
15
+
16
+ 'use strict';
17
+
18
+ const { estimateTokens } = require('../utils/tokens');
19
+
20
+ /** Memory bridge timeout in milliseconds. */
21
+ const BRIDGE_TIMEOUT_MS = 15;
22
+
23
+ /**
24
+ * Bracket-to-memory-layer mapping.
25
+ *
26
+ * FRESH → skip (no memory needed)
27
+ * MODERATE → Layer 1 metadata (~50 tokens)
28
+ * DEPLETED → Layer 2 chunks (~200 tokens)
29
+ * CRITICAL → Layer 3 full content (~1000 tokens)
30
+ */
31
+ const BRACKET_LAYER_MAP = {
32
+ FRESH: { layer: 0, maxTokens: 0 },
33
+ MODERATE: { layer: 1, maxTokens: 50 },
34
+ DEPLETED: { layer: 2, maxTokens: 200 },
35
+ CRITICAL: { layer: 3, maxTokens: 1000 },
36
+ };
37
+
38
+ /** Default sector for unknown agents. */
39
+ const DEFAULT_SECTORS = ['semantic'];
40
+
41
+ /**
42
+ * MemoryBridge — Feature-gated MIS consumer.
43
+ *
44
+ * Provides bracket-aware memory retrieval with:
45
+ * - Feature gate check (sync, <1ms)
46
+ * - Agent-scoped sector filtering
47
+ * - Token budget enforcement
48
+ * - Session-level caching
49
+ * - Timeout protection (<15ms)
50
+ * - Error catch-all with warn-and-proceed
51
+ */
52
+ class MemoryBridge {
53
+ /**
54
+ * @param {object} [options={}]
55
+ * @param {number} [options.timeout=15] - Max execution time in ms
56
+ */
57
+ constructor(options = {}) {
58
+ this._timeout = options.timeout || BRIDGE_TIMEOUT_MS;
59
+ this._provider = null;
60
+ this._featureGate = null;
61
+ this._initialized = false;
62
+ }
63
+
64
+ /**
65
+ * Lazy-load feature gate and provider.
66
+ * Isolates pro dependency to runtime only.
67
+ *
68
+ * @private
69
+ */
70
+ _init() {
71
+ if (this._initialized) return;
72
+ this._initialized = true;
73
+
74
+ try {
75
+ const { featureGate } = require('../../../../pro/license/feature-gate');
76
+ this._featureGate = featureGate;
77
+ } catch {
78
+ // Pro not installed — feature gate unavailable
79
+ this._featureGate = null;
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Lazy-load the SynapseMemoryProvider (pro).
85
+ * Only loaded when feature gate confirms availability.
86
+ *
87
+ * @private
88
+ * @returns {object|null} Provider instance or null
89
+ */
90
+ _getProvider() {
91
+ if (this._provider) return this._provider;
92
+
93
+ try {
94
+ const { SynapseMemoryProvider } = require('../../../../pro/memory/synapse-memory-provider');
95
+ this._provider = new SynapseMemoryProvider();
96
+ return this._provider;
97
+ } catch {
98
+ // Provider not available
99
+ return null;
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Get memory hints for the current prompt context.
105
+ *
106
+ * Returns an array of memory hint objects suitable for injection
107
+ * into the SYNAPSE pipeline. Gracefully returns [] when:
108
+ * - Pro feature is unavailable
109
+ * - Bracket is FRESH (no memory needed)
110
+ * - Provider fails or times out
111
+ * - Any error occurs
112
+ *
113
+ * @param {string} agentId - Active agent ID (e.g., 'dev', 'qa')
114
+ * @param {string} bracket - Context bracket (FRESH, MODERATE, DEPLETED, CRITICAL)
115
+ * @param {number} tokenBudget - Max tokens available for memory hints
116
+ * @returns {Promise<Array<{content: string, source: string, relevance: number, tokens: number}>>}
117
+ */
118
+ async getMemoryHints(agentId, bracket, tokenBudget) {
119
+ try {
120
+ // 1. Feature gate check (sync, <1ms)
121
+ this._init();
122
+ if (!this._featureGate || !this._featureGate.isAvailable('pro.memory.synapse')) {
123
+ return [];
124
+ }
125
+
126
+ // 2. Bracket check — FRESH needs no memory
127
+ const bracketConfig = BRACKET_LAYER_MAP[bracket];
128
+ if (!bracketConfig || bracketConfig.layer === 0) {
129
+ return [];
130
+ }
131
+
132
+ // 3. Calculate effective token budget
133
+ const effectiveBudget = Math.min(
134
+ bracketConfig.maxTokens,
135
+ tokenBudget > 0 ? tokenBudget : bracketConfig.maxTokens,
136
+ );
137
+
138
+ if (effectiveBudget <= 0) {
139
+ return [];
140
+ }
141
+
142
+ // 4. Load provider
143
+ const provider = this._getProvider();
144
+ if (!provider) {
145
+ return [];
146
+ }
147
+
148
+ // 5. Execute with timeout protection
149
+ const hints = await this._executeWithTimeout(
150
+ () => provider.getMemories(agentId, bracket, effectiveBudget),
151
+ this._timeout,
152
+ );
153
+
154
+ // 6. Enforce token budget on results
155
+ return this._enforceTokenBudget(hints || [], effectiveBudget);
156
+ } catch (error) {
157
+ // Catch-all: warn and proceed with empty results
158
+ console.warn(`[synapse:memory-bridge] Error getting memory hints: ${error.message}`);
159
+ return [];
160
+ }
161
+ }
162
+
163
+ /**
164
+ * Execute a function with timeout protection.
165
+ *
166
+ * @private
167
+ * @param {Function} fn - Async function to execute
168
+ * @param {number} timeoutMs - Timeout in milliseconds
169
+ * @returns {Promise<*>} Result or empty array on timeout
170
+ */
171
+ async _executeWithTimeout(fn, timeoutMs) {
172
+ return new Promise((resolve) => {
173
+ const timer = setTimeout(() => {
174
+ console.warn(`[synapse:memory-bridge] Timeout after ${timeoutMs}ms`);
175
+ resolve([]);
176
+ }, timeoutMs);
177
+
178
+ Promise.resolve(fn())
179
+ .then((result) => {
180
+ clearTimeout(timer);
181
+ resolve(result);
182
+ })
183
+ .catch((error) => {
184
+ clearTimeout(timer);
185
+ console.warn(`[synapse:memory-bridge] Provider error: ${error.message}`);
186
+ resolve([]);
187
+ });
188
+ });
189
+ }
190
+
191
+ /**
192
+ * Enforce token budget on hint array.
193
+ * Removes hints from end until within budget.
194
+ *
195
+ * @private
196
+ * @param {Array<{content: string, tokens: number}>} hints
197
+ * @param {number} budget
198
+ * @returns {Array}
199
+ */
200
+ _enforceTokenBudget(hints, budget) {
201
+ if (!Array.isArray(hints) || hints.length === 0) {
202
+ return [];
203
+ }
204
+
205
+ const result = [];
206
+ let tokensUsed = 0;
207
+
208
+ for (const hint of hints) {
209
+ const hintTokens = hint.tokens || estimateTokens(hint.content || '');
210
+ if (tokensUsed + hintTokens > budget) {
211
+ break;
212
+ }
213
+ result.push({ ...hint, tokens: hintTokens });
214
+ tokensUsed += hintTokens;
215
+ }
216
+
217
+ return result;
218
+ }
219
+
220
+ /**
221
+ * Clear provider cache. Used for testing and session reset.
222
+ */
223
+ clearCache() {
224
+ if (this._provider && typeof this._provider.clearCache === 'function') {
225
+ this._provider.clearCache();
226
+ }
227
+ }
228
+
229
+ /**
230
+ * Reset internal state. Used for testing.
231
+ *
232
+ * @private
233
+ */
234
+ _reset() {
235
+ this._provider = null;
236
+ this._featureGate = null;
237
+ this._initialized = false;
238
+ }
239
+ }
240
+
241
+ module.exports = {
242
+ MemoryBridge,
243
+ BRACKET_LAYER_MAP,
244
+ BRIDGE_TIMEOUT_MS,
245
+ DEFAULT_SECTORS,
246
+ };
@@ -10,6 +10,8 @@
10
10
  * @created Story SYN-6 - SynapseEngine Orchestrator + Output Formatter
11
11
  */
12
12
 
13
+ const { estimateTokens } = require('../utils/tokens');
14
+
13
15
  // ---------------------------------------------------------------------------
14
16
  // Section ordering (DESIGN doc section 14)
15
17
  // ---------------------------------------------------------------------------
@@ -29,6 +31,7 @@ const SECTION_ORDER = [
29
31
  'TASK',
30
32
  'SQUAD',
31
33
  'KEYWORD',
34
+ 'MEMORY_HINTS',
32
35
  'STAR_COMMANDS',
33
36
  'DEVMODE',
34
37
  'SUMMARY',
@@ -45,6 +48,7 @@ const LAYER_TO_SECTION = {
45
48
  task: 'TASK',
46
49
  squad: 'SQUAD',
47
50
  keyword: 'KEYWORD',
51
+ memory: 'MEMORY_HINTS',
48
52
  'star-command': 'STAR_COMMANDS',
49
53
  };
50
54
 
@@ -237,6 +241,34 @@ function formatStarCommands(result) {
237
241
  return lines.join('\n');
238
242
  }
239
243
 
244
+ // ---------------------------------------------------------------------------
245
+ // Memory Hints Section (SYN-10)
246
+ // ---------------------------------------------------------------------------
247
+
248
+ /**
249
+ * Format the MEMORY HINTS section.
250
+ *
251
+ * Only included when hints array is non-empty.
252
+ * Each hint displays source, relevance, and content.
253
+ *
254
+ * @param {object} result - Layer result { rules (hint objects), metadata }
255
+ * @returns {string}
256
+ */
257
+ function formatMemoryHints(result) {
258
+ const lines = ['[MEMORY HINTS]'];
259
+
260
+ for (const hint of result.rules) {
261
+ const source = hint.source || 'memory';
262
+ const relevance = typeof hint.relevance === 'number'
263
+ ? `${(hint.relevance * 100).toFixed(0)}%`
264
+ : '?%';
265
+ const content = hint.content || '';
266
+ lines.push(` [${source}] (relevance: ${relevance}) ${content}`);
267
+ }
268
+
269
+ return lines.join('\n');
270
+ }
271
+
240
272
  // ---------------------------------------------------------------------------
241
273
  // DEVMODE Section (DESIGN doc section 13)
242
274
  // ---------------------------------------------------------------------------
@@ -346,18 +378,6 @@ function formatSummary(results, _metrics) {
346
378
  // Token Budget Enforcement
347
379
  // ---------------------------------------------------------------------------
348
380
 
349
- /**
350
- * Estimate the number of tokens from a string.
351
- *
352
- * Uses the proven heuristic: tokens ~ string.length / 4
353
- *
354
- * @param {string} text - Text to estimate
355
- * @returns {number} Estimated token count
356
- */
357
- function estimateTokens(text) {
358
- return Math.ceil((text || '').length / 4);
359
- }
360
-
361
381
  /**
362
382
  * Enforce a token budget by removing sections from the end.
363
383
  *
@@ -381,6 +401,7 @@ function enforceTokenBudget(sections, sectionIds, tokenBudget) {
381
401
  const TRUNCATION_ORDER = [
382
402
  'SUMMARY',
383
403
  'KEYWORD',
404
+ 'MEMORY_HINTS',
384
405
  'SQUAD',
385
406
  'STAR_COMMANDS',
386
407
  'DEVMODE',
@@ -428,6 +449,7 @@ const SECTION_FORMATTERS = {
428
449
  TASK: formatTask,
429
450
  SQUAD: formatSquad,
430
451
  KEYWORD: formatKeyword,
452
+ MEMORY_HINTS: formatMemoryHints,
431
453
  STAR_COMMANDS: formatStarCommands,
432
454
  };
433
455