thumbgate 0.9.9

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 (369) hide show
  1. package/.claude-plugin/README.md +134 -0
  2. package/.claude-plugin/bundle/icon.png +0 -0
  3. package/.claude-plugin/bundle/icon.svg +18 -0
  4. package/.claude-plugin/bundle/server/index.js +24 -0
  5. package/.claude-plugin/marketplace.json +36 -0
  6. package/.claude-plugin/plugin.json +21 -0
  7. package/.well-known/mcp/server-card.json +231 -0
  8. package/LICENSE +21 -0
  9. package/README.md +375 -0
  10. package/adapters/README.md +9 -0
  11. package/adapters/amp/skills/rlhf-feedback/SKILL.md +22 -0
  12. package/adapters/chatgpt/INSTALL.md +83 -0
  13. package/adapters/chatgpt/openapi.yaml +1281 -0
  14. package/adapters/claude/.mcp.json +14 -0
  15. package/adapters/codex/config.toml +9 -0
  16. package/adapters/gemini/function-declarations.json +224 -0
  17. package/adapters/mcp/server-stdio.js +788 -0
  18. package/adapters/opencode/opencode.json +15 -0
  19. package/bin/cli.js +1483 -0
  20. package/bin/memory.sh +64 -0
  21. package/bin/obsidian-sync.sh +20 -0
  22. package/bin/postinstall.js +37 -0
  23. package/config/build-metadata.json +4 -0
  24. package/config/e2e-critical-flows.json +45 -0
  25. package/config/gate-templates.json +77 -0
  26. package/config/gates/claim-verification.json +29 -0
  27. package/config/gates/computer-use.json +39 -0
  28. package/config/gates/default.json +117 -0
  29. package/config/github-about.json +25 -0
  30. package/config/mcp-allowlists.json +135 -0
  31. package/config/model-tiers.json +33 -0
  32. package/config/partner-routing.json +132 -0
  33. package/config/policy-bundles/constrained-v1.json +64 -0
  34. package/config/policy-bundles/default-v1.json +91 -0
  35. package/config/rubrics/default-v1.json +52 -0
  36. package/config/skill-packs/react-testing.json +23 -0
  37. package/config/skill-packs/stripe-integration/references/api-spec.json +1 -0
  38. package/config/skill-packs/stripe-integration/references/webhook-guide.md +3 -0
  39. package/config/skill-specs/pr-reviewer.json +9 -0
  40. package/config/skill-specs/release-status.json +9 -0
  41. package/config/skill-specs/ticket-triage.json +9 -0
  42. package/config/subagent-profiles.json +32 -0
  43. package/config/tessl-tiles.json +29 -0
  44. package/config/thumbgate-settings.managed.json +12 -0
  45. package/openapi/openapi.yaml +1281 -0
  46. package/package.json +286 -0
  47. package/plugins/amp-skill/INSTALL.md +52 -0
  48. package/plugins/amp-skill/SKILL.md +64 -0
  49. package/plugins/claude-codex-bridge/.claude-plugin/plugin.json +22 -0
  50. package/plugins/claude-codex-bridge/.mcp.json +12 -0
  51. package/plugins/claude-codex-bridge/INSTALL.md +43 -0
  52. package/plugins/claude-codex-bridge/README.md +46 -0
  53. package/plugins/claude-codex-bridge/scripts/codex-bridge.js +288 -0
  54. package/plugins/claude-codex-bridge/skills/adversarial-review/SKILL.md +24 -0
  55. package/plugins/claude-codex-bridge/skills/result/SKILL.md +22 -0
  56. package/plugins/claude-codex-bridge/skills/review/SKILL.md +28 -0
  57. package/plugins/claude-codex-bridge/skills/second-pass/SKILL.md +27 -0
  58. package/plugins/claude-codex-bridge/skills/setup/SKILL.md +21 -0
  59. package/plugins/claude-codex-bridge/skills/status/SKILL.md +19 -0
  60. package/plugins/claude-skill/INSTALL.md +55 -0
  61. package/plugins/claude-skill/SKILL.md +46 -0
  62. package/plugins/codex-profile/.codex-plugin/plugin.json +43 -0
  63. package/plugins/codex-profile/.mcp.json +12 -0
  64. package/plugins/codex-profile/AGENTS.md +20 -0
  65. package/plugins/codex-profile/INSTALL.md +66 -0
  66. package/plugins/codex-profile/README.md +37 -0
  67. package/plugins/cursor-marketplace/.cursor-plugin/plugin.json +23 -0
  68. package/plugins/cursor-marketplace/CHANGELOG.md +30 -0
  69. package/plugins/cursor-marketplace/LICENSE +21 -0
  70. package/plugins/cursor-marketplace/README.md +124 -0
  71. package/plugins/cursor-marketplace/agents/reliability-reviewer.md +31 -0
  72. package/plugins/cursor-marketplace/assets/logo-400x400.png +0 -0
  73. package/plugins/cursor-marketplace/commands/capture-feedback.md +33 -0
  74. package/plugins/cursor-marketplace/commands/check-gates.md +25 -0
  75. package/plugins/cursor-marketplace/commands/show-lessons.md +27 -0
  76. package/plugins/cursor-marketplace/hooks/hooks.json +10 -0
  77. package/plugins/cursor-marketplace/mcp.json +12 -0
  78. package/plugins/cursor-marketplace/rules/feedback-capture.mdc +34 -0
  79. package/plugins/cursor-marketplace/rules/pre-action-gates.mdc +30 -0
  80. package/plugins/cursor-marketplace/rules/session-continuity.mdc +28 -0
  81. package/plugins/cursor-marketplace/scripts/gate-check.sh +11 -0
  82. package/plugins/cursor-marketplace/skills/capture-feedback/SKILL.md +47 -0
  83. package/plugins/cursor-marketplace/skills/prevention-rules/SKILL.md +31 -0
  84. package/plugins/cursor-marketplace/skills/recall-context/SKILL.md +30 -0
  85. package/plugins/cursor-marketplace/skills/search-lessons/SKILL.md +33 -0
  86. package/plugins/gemini-extension/INSTALL.md +92 -0
  87. package/plugins/gemini-extension/gemini_prompt.txt +14 -0
  88. package/plugins/gemini-extension/tool_contract.json +45 -0
  89. package/plugins/opencode-profile/INSTALL.md +57 -0
  90. package/public/assets/instagram-card.png +0 -0
  91. package/public/assets/tiktok-agent-memory.mp4 +0 -0
  92. package/public/blog.html +400 -0
  93. package/public/dashboard.html +1093 -0
  94. package/public/guide.html +317 -0
  95. package/public/index.html +1195 -0
  96. package/public/learn/agent-harness-pattern.html +180 -0
  97. package/public/learn/ai-agent-persistent-memory.html +202 -0
  98. package/public/learn/learn.css +45 -0
  99. package/public/learn/mcp-pre-action-gates-explained.html +172 -0
  100. package/public/learn/stop-ai-agent-force-push.html +134 -0
  101. package/public/learn/vibe-coding-safety-net.html +142 -0
  102. package/public/learn.html +213 -0
  103. package/public/lessons.html +650 -0
  104. package/public/vercel.json +8 -0
  105. package/scripts/__pycache__/train_from_feedback.cpython-314.pyc +0 -0
  106. package/scripts/a2ui-engine.js +73 -0
  107. package/scripts/access-anomaly-detector.js +12 -0
  108. package/scripts/adk-consolidator.js +266 -0
  109. package/scripts/agent-readiness.js +220 -0
  110. package/scripts/agent-security-hardening.js +227 -0
  111. package/scripts/agentic-data-pipeline.js +847 -0
  112. package/scripts/analytics-report.js +328 -0
  113. package/scripts/analytics-window.js +158 -0
  114. package/scripts/async-job-runner.js +1001 -0
  115. package/scripts/audit-trail.js +398 -0
  116. package/scripts/auto-promote-gates.js +293 -0
  117. package/scripts/auto-wire-hooks.js +316 -0
  118. package/scripts/autonomous-sales-agent.js +39 -0
  119. package/scripts/autoresearch-runner.js +216 -0
  120. package/scripts/background-agent-governance.js +237 -0
  121. package/scripts/behavioral-extraction.js +93 -0
  122. package/scripts/belief-update.js +84 -0
  123. package/scripts/billing.js +2438 -0
  124. package/scripts/bot-detector.js +50 -0
  125. package/scripts/budget-guard.js +173 -0
  126. package/scripts/build-claude-mcpb.js +189 -0
  127. package/scripts/build-metadata.js +97 -0
  128. package/scripts/check-congruence.js +322 -0
  129. package/scripts/cli-feedback.js +135 -0
  130. package/scripts/cli-telemetry.js +87 -0
  131. package/scripts/cloudflare-dynamic-sandbox.js +315 -0
  132. package/scripts/code-reasoning.js +350 -0
  133. package/scripts/codegraph-context.js +466 -0
  134. package/scripts/commercial-offer.js +56 -0
  135. package/scripts/computer-use-firewall.js +250 -0
  136. package/scripts/context-engine.js +694 -0
  137. package/scripts/contextfs.js +1287 -0
  138. package/scripts/conversation-context.js +119 -0
  139. package/scripts/creator-campaigns.js +239 -0
  140. package/scripts/daemon-manager.js +108 -0
  141. package/scripts/daily-digest.js +11 -0
  142. package/scripts/dashboard-render-spec.js +395 -0
  143. package/scripts/dashboard.js +1058 -0
  144. package/scripts/data-governance.js +173 -0
  145. package/scripts/delegation-runtime.js +900 -0
  146. package/scripts/deploy-gcp.sh +44 -0
  147. package/scripts/deploy-policy.js +231 -0
  148. package/scripts/disagreement-mining.js +315 -0
  149. package/scripts/dispatch-brief.js +159 -0
  150. package/scripts/distribution-surfaces.js +44 -0
  151. package/scripts/dpo-optimizer.js +206 -0
  152. package/scripts/ensure-repo-bootstrap.js +129 -0
  153. package/scripts/ephemeral-agent-store.js +219 -0
  154. package/scripts/eval-harness.js +56 -0
  155. package/scripts/evolution-state.js +241 -0
  156. package/scripts/experiment-tracker.js +267 -0
  157. package/scripts/export-databricks-bundle.js +242 -0
  158. package/scripts/export-dpo-pairs.js +344 -0
  159. package/scripts/export-kto-pairs.js +309 -0
  160. package/scripts/export-training.js +450 -0
  161. package/scripts/failure-diagnostics.js +558 -0
  162. package/scripts/feedback-attribution.js +313 -0
  163. package/scripts/feedback-fallback.js +110 -0
  164. package/scripts/feedback-history-distiller.js +391 -0
  165. package/scripts/feedback-inbox-read.js +162 -0
  166. package/scripts/feedback-loop.js +1887 -0
  167. package/scripts/feedback-paths.js +145 -0
  168. package/scripts/feedback-quality.js +139 -0
  169. package/scripts/feedback-root-consolidator.js +238 -0
  170. package/scripts/feedback-schema.js +426 -0
  171. package/scripts/feedback-session.js +286 -0
  172. package/scripts/feedback-to-memory.js +185 -0
  173. package/scripts/feedback-to-rules.js +164 -0
  174. package/scripts/filesystem-search.js +405 -0
  175. package/scripts/funnel-analytics.js +35 -0
  176. package/scripts/gate-satisfy.js +42 -0
  177. package/scripts/gate-stats.js +116 -0
  178. package/scripts/gate-templates.js +70 -0
  179. package/scripts/gates-engine.js +816 -0
  180. package/scripts/generate-paperbanana-diagrams.sh +99 -0
  181. package/scripts/generate-pretool-hook.sh +40 -0
  182. package/scripts/github-about.js +350 -0
  183. package/scripts/github-outreach.js +65 -0
  184. package/scripts/gtm-revenue-loop.js +520 -0
  185. package/scripts/hallucination-detector.js +226 -0
  186. package/scripts/hf-papers.js +317 -0
  187. package/scripts/history-distiller.js +200 -0
  188. package/scripts/hook-auto-capture.sh +100 -0
  189. package/scripts/hook-stop-pr-thread-check.sh +68 -0
  190. package/scripts/hook-stop-self-score.sh +51 -0
  191. package/scripts/hook-stop-verify-deploy.sh +31 -0
  192. package/scripts/hook-thumbgate-cache-updater.js +48 -0
  193. package/scripts/hook-verify-before-done.sh +20 -0
  194. package/scripts/hosted-config.js +156 -0
  195. package/scripts/hybrid-feedback-context.js +675 -0
  196. package/scripts/install-mcp.js +159 -0
  197. package/scripts/intent-router.js +392 -0
  198. package/scripts/internal-agent-bootstrap.js +490 -0
  199. package/scripts/jsonl-watcher.js +155 -0
  200. package/scripts/lesson-db.js +613 -0
  201. package/scripts/lesson-inference.js +310 -0
  202. package/scripts/lesson-retrieval.js +95 -0
  203. package/scripts/lesson-rotation.js +137 -0
  204. package/scripts/lesson-search.js +644 -0
  205. package/scripts/lesson-synthesis.js +196 -0
  206. package/scripts/license.js +50 -0
  207. package/scripts/local-model-profile.js +384 -0
  208. package/scripts/markdown-escape.js +12 -0
  209. package/scripts/marketing-experiment.js +671 -0
  210. package/scripts/mcp-config.js +149 -0
  211. package/scripts/mcp-policy.js +99 -0
  212. package/scripts/memalign-recall.js +111 -0
  213. package/scripts/memory-firewall.js +222 -0
  214. package/scripts/memory-migration.js +296 -0
  215. package/scripts/meta-policy.js +190 -0
  216. package/scripts/metered-billing.js +16 -0
  217. package/scripts/model-tier-router.js +301 -0
  218. package/scripts/money-watcher.js +71 -0
  219. package/scripts/multi-hop-recall.js +240 -0
  220. package/scripts/natural-language-harness.js +330 -0
  221. package/scripts/obsidian-export.js +713 -0
  222. package/scripts/operational-dashboard.js +103 -0
  223. package/scripts/operational-summary.js +93 -0
  224. package/scripts/optimize-context.js +17 -0
  225. package/scripts/org-dashboard.js +201 -0
  226. package/scripts/partner-orchestration.js +146 -0
  227. package/scripts/per-step-scoring.js +165 -0
  228. package/scripts/perplexity-marketing.js +466 -0
  229. package/scripts/pii-scanner.js +153 -0
  230. package/scripts/plan-gate.js +154 -0
  231. package/scripts/post-everywhere.js +308 -0
  232. package/scripts/post-to-x-retry.sh +22 -0
  233. package/scripts/post-to-x.js +369 -0
  234. package/scripts/pr-manager.js +236 -0
  235. package/scripts/predictive-insights.js +356 -0
  236. package/scripts/principle-extractor.js +162 -0
  237. package/scripts/pro-features.js +40 -0
  238. package/scripts/pro-local-dashboard.js +174 -0
  239. package/scripts/problem-detail.js +53 -0
  240. package/scripts/product-feedback.js +134 -0
  241. package/scripts/profile-router.js +245 -0
  242. package/scripts/prompt-dlp.js +221 -0
  243. package/scripts/prompt-guard.js +83 -0
  244. package/scripts/prove-adapters.js +863 -0
  245. package/scripts/prove-attribution.js +365 -0
  246. package/scripts/prove-automation.js +653 -0
  247. package/scripts/prove-autoresearch.js +304 -0
  248. package/scripts/prove-claim-verification.js +277 -0
  249. package/scripts/prove-cloudflare-sandbox.js +163 -0
  250. package/scripts/prove-data-pipeline.js +410 -0
  251. package/scripts/prove-data-quality.js +227 -0
  252. package/scripts/prove-evolution.js +352 -0
  253. package/scripts/prove-harnesses.js +287 -0
  254. package/scripts/prove-intelligence.js +259 -0
  255. package/scripts/prove-lancedb.js +371 -0
  256. package/scripts/prove-local-intelligence.js +342 -0
  257. package/scripts/prove-loop-closure.js +263 -0
  258. package/scripts/prove-predictive-insights.js +357 -0
  259. package/scripts/prove-runtime.js +350 -0
  260. package/scripts/prove-seo-gsd.js +234 -0
  261. package/scripts/prove-settings.js +279 -0
  262. package/scripts/prove-subway-upgrades.js +277 -0
  263. package/scripts/prove-tessl.js +229 -0
  264. package/scripts/prove-training-export.js +327 -0
  265. package/scripts/prove-workflow-contract.js +116 -0
  266. package/scripts/prove-xmemory.js +332 -0
  267. package/scripts/publish-decision.js +133 -0
  268. package/scripts/pulse.js +80 -0
  269. package/scripts/rate-limiter.js +125 -0
  270. package/scripts/reddit-dm-outreach.js +182 -0
  271. package/scripts/reddit-monitor-cron.sh +26 -0
  272. package/scripts/reflector-agent.js +221 -0
  273. package/scripts/reminder-engine.js +132 -0
  274. package/scripts/revenue-status.js +472 -0
  275. package/scripts/risk-scorer.js +459 -0
  276. package/scripts/rlaif-self-audit.js +129 -0
  277. package/scripts/rlhf_session_start.sh +32 -0
  278. package/scripts/rubric-engine.js +230 -0
  279. package/scripts/schedule-manager.js +251 -0
  280. package/scripts/secret-scanner.js +414 -0
  281. package/scripts/self-heal.js +147 -0
  282. package/scripts/self-healing-check.js +188 -0
  283. package/scripts/semantic-layer.js +98 -0
  284. package/scripts/seo-gsd.js +1153 -0
  285. package/scripts/settings-hierarchy.js +214 -0
  286. package/scripts/shieldcortex-memory-firewall-runner.mjs +53 -0
  287. package/scripts/skill-exporter.js +262 -0
  288. package/scripts/skill-generator.js +446 -0
  289. package/scripts/skill-materializer.js +134 -0
  290. package/scripts/skill-packs.js +136 -0
  291. package/scripts/skill-proposer.js +99 -0
  292. package/scripts/skill-quality-tracker.js +282 -0
  293. package/scripts/slo-alert-engine.js +14 -0
  294. package/scripts/slow-loop.js +72 -0
  295. package/scripts/social-analytics/db/schema.sql +32 -0
  296. package/scripts/social-analytics/db/social-analytics.db +0 -0
  297. package/scripts/social-analytics/digest.js +256 -0
  298. package/scripts/social-analytics/generate-instagram-card.js +97 -0
  299. package/scripts/social-analytics/instagram-thumbgate-post.js +107 -0
  300. package/scripts/social-analytics/load-env.js +46 -0
  301. package/scripts/social-analytics/mcp-server.js +289 -0
  302. package/scripts/social-analytics/normalizer.js +580 -0
  303. package/scripts/social-analytics/notify.js +162 -0
  304. package/scripts/social-analytics/poll-all.js +92 -0
  305. package/scripts/social-analytics/pollers/github.js +195 -0
  306. package/scripts/social-analytics/pollers/instagram.js +253 -0
  307. package/scripts/social-analytics/pollers/linkedin.js +330 -0
  308. package/scripts/social-analytics/pollers/plausible.js +247 -0
  309. package/scripts/social-analytics/pollers/reddit.js +306 -0
  310. package/scripts/social-analytics/pollers/threads.js +233 -0
  311. package/scripts/social-analytics/pollers/tiktok.js +203 -0
  312. package/scripts/social-analytics/pollers/x.js +227 -0
  313. package/scripts/social-analytics/pollers/youtube.js +304 -0
  314. package/scripts/social-analytics/pollers/zernio.js +183 -0
  315. package/scripts/social-analytics/publish-instagram-thumbgate.js +98 -0
  316. package/scripts/social-analytics/publish-thumbgate-launch.js +316 -0
  317. package/scripts/social-analytics/publishers/devto.js +122 -0
  318. package/scripts/social-analytics/publishers/instagram.js +317 -0
  319. package/scripts/social-analytics/publishers/linkedin.js +294 -0
  320. package/scripts/social-analytics/publishers/reddit.js +390 -0
  321. package/scripts/social-analytics/publishers/threads.js +275 -0
  322. package/scripts/social-analytics/publishers/tiktok.js +217 -0
  323. package/scripts/social-analytics/publishers/x.js +259 -0
  324. package/scripts/social-analytics/publishers/youtube.js +223 -0
  325. package/scripts/social-analytics/publishers/zernio.js +378 -0
  326. package/scripts/social-analytics/run-digest.js +34 -0
  327. package/scripts/social-analytics/store.js +257 -0
  328. package/scripts/social-analytics/utm.js +143 -0
  329. package/scripts/social-pipeline.js +2628 -0
  330. package/scripts/social-quality-gate.js +18 -0
  331. package/scripts/social-reply-monitor.js +445 -0
  332. package/scripts/status-dashboard.js +155 -0
  333. package/scripts/statusline-lesson.js +16 -0
  334. package/scripts/statusline-tower.js +8 -0
  335. package/scripts/statusline.sh +116 -0
  336. package/scripts/stripe-live-status.js +115 -0
  337. package/scripts/subagent-profiles.js +79 -0
  338. package/scripts/sync-gh-secrets-from-env.sh +70 -0
  339. package/scripts/sync-github-about.js +52 -0
  340. package/scripts/sync-version.js +447 -0
  341. package/scripts/synthetic-dpo.js +234 -0
  342. package/scripts/telemetry-analytics.js +821 -0
  343. package/scripts/tessl-export.js +371 -0
  344. package/scripts/test-coverage.js +120 -0
  345. package/scripts/thompson-sampling.js +417 -0
  346. package/scripts/thumbgate-search.js +189 -0
  347. package/scripts/tool-kpi-tracker.js +12 -0
  348. package/scripts/tool-registry.js +811 -0
  349. package/scripts/train_from_feedback.py +933 -0
  350. package/scripts/user-profile.js +78 -0
  351. package/scripts/validate-feedback.js +581 -0
  352. package/scripts/validate-workflow-contract.js +287 -0
  353. package/scripts/vector-store.js +197 -0
  354. package/scripts/verification-loop.js +291 -0
  355. package/scripts/verify-obsidian-setup.sh +269 -0
  356. package/scripts/verify-run.js +269 -0
  357. package/scripts/webhook-delivery.js +62 -0
  358. package/scripts/weekly-auto-post.js +124 -0
  359. package/scripts/workflow-runs.js +154 -0
  360. package/scripts/workflow-sprint-intake.js +475 -0
  361. package/scripts/workspace-evolver.js +374 -0
  362. package/scripts/x-autonomous-marketing.js +139 -0
  363. package/scripts/xmemory-lite.js +405 -0
  364. package/skills/agent-memory/SKILL.md +97 -0
  365. package/skills/rlhf-feedback/SKILL.md +49 -0
  366. package/skills/solve-architecture-autonomy/SKILL.md +17 -0
  367. package/skills/solve-architecture-autonomy/tool.js +33 -0
  368. package/skills/thumbgate/SKILL.md +114 -0
  369. package/src/api/server.js +4206 -0
@@ -0,0 +1,1287 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * ContextFS
4
+ *
5
+ * Persistent, file-system-native context store implementing:
6
+ * - Constructor: build relevant context pack
7
+ * - Loader: enforce bounded context size
8
+ * - Evaluator: record pack outcome for learning loop
9
+ */
10
+
11
+ const fs = require('fs');
12
+ const path = require('path');
13
+ const crypto = require('crypto');
14
+ const { resolveFeedbackDir } = require('./feedback-paths');
15
+ const {
16
+ retrieveHierarchicalDocuments,
17
+ shouldUseHierarchicalRetrieval,
18
+ } = require('./xmemory-lite');
19
+
20
+ const CONTEXTFS_RETRIEVAL_VERSION = 'xmemory-lite-v1';
21
+
22
+ function getFeedbackBaseDir() {
23
+ return resolveFeedbackDir();
24
+ }
25
+
26
+ const FEEDBACK_DIR = getFeedbackBaseDir();
27
+ const CONTEXTFS_ROOT = process.env.THUMBGATE_CONTEXTFS_DIR
28
+ || (FEEDBACK_DIR.endsWith('contextfs') ? FEEDBACK_DIR : path.join(FEEDBACK_DIR, 'contextfs'));
29
+
30
+ const NAMESPACES = {
31
+ rawHistory: 'raw_history',
32
+ memoryError: path.join('memory', 'error'),
33
+ memoryLearning: path.join('memory', 'learning'),
34
+ rules: 'rules',
35
+ tools: 'tools',
36
+ provenance: 'provenance',
37
+ session: 'session',
38
+ research: 'research',
39
+ };
40
+ const DEFAULT_SEARCH_NAMESPACES = [
41
+ NAMESPACES.memoryError,
42
+ NAMESPACES.memoryLearning,
43
+ NAMESPACES.rules,
44
+ NAMESPACES.rawHistory,
45
+ ];
46
+ const NAMESPACE_ALIAS_MAP = new Map([
47
+ ...Object.entries(NAMESPACES).map(([key, value]) => [key, value]),
48
+ ...Object.values(NAMESPACES).map((value) => [value, value]),
49
+ ]);
50
+
51
+ const PACK_TEMPLATES = {
52
+ 'bug-investigation': {
53
+ namespaces: ['memoryError', 'rules'],
54
+ maxItems: 10,
55
+ maxChars: 8000,
56
+ queryPrefix: 'bug failure error crash',
57
+ },
58
+ 'session-resume': {
59
+ namespaces: ['session', 'memoryLearning', 'rules'],
60
+ maxItems: 8,
61
+ maxChars: 6000,
62
+ queryPrefix: 'session context resume',
63
+ },
64
+ 'sales-call-prep': {
65
+ namespaces: ['memoryLearning', 'rules'],
66
+ maxItems: 6,
67
+ maxChars: 4000,
68
+ queryPrefix: 'value proof evidence workflow',
69
+ },
70
+ 'competitor-scan': {
71
+ namespaces: ['memoryLearning', 'memoryError'],
72
+ maxItems: 8,
73
+ maxChars: 6000,
74
+ queryPrefix: 'competitor comparison alternative',
75
+ },
76
+ 'research-brief': {
77
+ namespaces: ['research'],
78
+ maxItems: 10,
79
+ maxChars: 8000,
80
+ queryPrefix: 'research paper',
81
+ },
82
+ 'autoresearch-brief': {
83
+ namespaces: ['research', 'memoryLearning', 'rules'],
84
+ maxItems: 12,
85
+ maxChars: 10000,
86
+ queryPrefix: 'research benchmark experiment reliability',
87
+ },
88
+ 'gtm-research': {
89
+ namespaces: ['research', 'memoryLearning'],
90
+ maxItems: 10,
91
+ maxChars: 8000,
92
+ queryPrefix: 'marketing conversion acquisition research',
93
+ },
94
+ };
95
+
96
+ function ensureDir(dirPath) {
97
+ if (!fs.existsSync(dirPath)) {
98
+ fs.mkdirSync(dirPath, { recursive: true });
99
+ }
100
+ }
101
+
102
+ function ensureContextFs() {
103
+ Object.values(NAMESPACES).forEach((subPath) => {
104
+ ensureDir(path.join(CONTEXTFS_ROOT, subPath));
105
+ });
106
+ }
107
+
108
+ function nowIso() {
109
+ return new Date().toISOString();
110
+ }
111
+
112
+ function inferNamespaceFromPath(filePath) {
113
+ if (!filePath) return '';
114
+ const relativeDir = path.relative(CONTEXTFS_ROOT, path.dirname(filePath));
115
+ if (!relativeDir || relativeDir.startsWith('..')) return '';
116
+ return relativeDir;
117
+ }
118
+
119
+ function toSlug(input) {
120
+ return String(input || 'item')
121
+ .toLowerCase()
122
+ .replace(/[^a-z0-9]+/g, '-')
123
+ .replace(/^-+|-+$/g, '')
124
+ .slice(0, 80) || 'item';
125
+ }
126
+
127
+ function writeJson(filePath, payload) {
128
+ ensureDir(path.dirname(filePath));
129
+ fs.writeFileSync(filePath, `${JSON.stringify(payload, null, 2)}\n`);
130
+ }
131
+
132
+ function appendJsonl(filePath, payload) {
133
+ ensureDir(path.dirname(filePath));
134
+ fs.appendFileSync(filePath, `${JSON.stringify(payload)}\n`);
135
+ }
136
+
137
+ function readJsonl(filePath) {
138
+ if (!fs.existsSync(filePath)) return [];
139
+ const raw = fs.readFileSync(filePath, 'utf-8').trim();
140
+ if (!raw) return [];
141
+ return raw
142
+ .split('\n')
143
+ .map((line) => {
144
+ try {
145
+ return JSON.parse(line);
146
+ } catch {
147
+ return null;
148
+ }
149
+ })
150
+ .filter(Boolean);
151
+ }
152
+
153
+ function listJsonFiles(dirPath) {
154
+ if (!fs.existsSync(dirPath)) return [];
155
+ const files = fs.readdirSync(dirPath, { withFileTypes: true });
156
+ const out = [];
157
+ files.forEach((entry) => {
158
+ const fullPath = path.join(dirPath, entry.name);
159
+ if (entry.isDirectory()) {
160
+ out.push(...listJsonFiles(fullPath));
161
+ return;
162
+ }
163
+ if (entry.isFile() && entry.name.endsWith('.json')) {
164
+ out.push(fullPath);
165
+ }
166
+ });
167
+ return out;
168
+ }
169
+
170
+ function tokenizeQuery(query) {
171
+ return String(query || '')
172
+ .toLowerCase()
173
+ .split(/[^a-z0-9]+/)
174
+ .filter(Boolean);
175
+ }
176
+
177
+ function uniqueTokens(tokens) {
178
+ return Array.from(new Set(tokens));
179
+ }
180
+
181
+ function querySimilarity(tokensA, tokensB) {
182
+ const setA = new Set(uniqueTokens(tokensA));
183
+ const setB = new Set(uniqueTokens(tokensB));
184
+ if (setA.size === 0 && setB.size === 0) return 1;
185
+ if (setA.size === 0 || setB.size === 0) return 0;
186
+
187
+ let intersection = 0;
188
+ for (const token of setA) {
189
+ if (setB.has(token)) intersection += 1;
190
+ }
191
+ const union = setA.size + setB.size - intersection;
192
+ return union === 0 ? 0 : intersection / union;
193
+ }
194
+
195
+ function buildSemanticCacheKey({ namespaces, maxItems, maxChars }) {
196
+ return JSON.stringify({
197
+ retrievalVersion: CONTEXTFS_RETRIEVAL_VERSION,
198
+ namespaces: normalizeNamespaces(namespaces),
199
+ maxItems,
200
+ maxChars,
201
+ });
202
+ }
203
+
204
+ function getSemanticCacheConfig() {
205
+ const enabled = process.env.THUMBGATE_SEMANTIC_CACHE_ENABLED !== 'false';
206
+ const thresholdRaw = Number(process.env.THUMBGATE_SEMANTIC_CACHE_THRESHOLD || '0.7');
207
+ const ttlSecondsRaw = Number(process.env.THUMBGATE_SEMANTIC_CACHE_TTL_SECONDS || '86400');
208
+ const threshold = Number.isFinite(thresholdRaw) ? Math.min(1, Math.max(0, thresholdRaw)) : 0.7;
209
+ const ttlSeconds = Number.isFinite(ttlSecondsRaw) ? Math.max(60, ttlSecondsRaw) : 86400;
210
+ return { enabled, threshold, ttlSeconds };
211
+ }
212
+
213
+ function getSemanticCachePath() {
214
+ return path.join(CONTEXTFS_ROOT, NAMESPACES.provenance, 'semantic-cache.jsonl');
215
+ }
216
+
217
+ function loadSemanticCacheEntries() {
218
+ return readJsonl(getSemanticCachePath());
219
+ }
220
+
221
+ function appendSemanticCacheEntry(entry) {
222
+ appendJsonl(getSemanticCachePath(), entry);
223
+ }
224
+
225
+ function getSourceHash(namespaces) {
226
+ const hasher = crypto.createHash('sha256');
227
+ const normalizedNamespaces = normalizeNamespaces(namespaces);
228
+
229
+ for (const ns of normalizedNamespaces) {
230
+ const dirPath = path.join(CONTEXTFS_ROOT, ns);
231
+ if (!fs.existsSync(dirPath)) continue;
232
+
233
+ const files = fs.readdirSync(dirPath).sort();
234
+ for (const file of files) {
235
+ if (file.endsWith('.json') || file.endsWith('.jsonl') || file.endsWith('.md')) {
236
+ const filePath = path.join(dirPath, file);
237
+ try {
238
+ const stats = fs.statSync(filePath);
239
+ hasher.update(`${file}:${stats.mtimeMs}:${stats.size}`);
240
+ } catch {
241
+ // Skip if file disappeared
242
+ }
243
+ }
244
+ }
245
+ }
246
+ return hasher.digest('hex');
247
+ }
248
+
249
+ function findSemanticCacheHit({ query, namespaces, maxItems, maxChars }) {
250
+ const { enabled, threshold, ttlSeconds } = getSemanticCacheConfig();
251
+ if (!enabled) return null;
252
+
253
+ const entries = loadSemanticCacheEntries();
254
+ if (entries.length === 0) return null;
255
+
256
+ const now = Date.now();
257
+ const queryTokens = tokenizeQuery(query);
258
+ const key = buildSemanticCacheKey({ namespaces, maxItems, maxChars });
259
+ const currentSourceHash = getSourceHash(namespaces);
260
+
261
+ for (let i = entries.length - 1; i >= 0; i -= 1) {
262
+ const entry = entries[i];
263
+ if (!entry || entry.key !== key || !entry.pack) continue;
264
+
265
+ // Zero-Waste Caching: validate source hash
266
+ if (entry.sourceHash !== currentSourceHash) {
267
+ continue;
268
+ }
269
+
270
+ const createdMs = new Date(entry.timestamp || 0).getTime();
271
+ if (Number.isFinite(createdMs) && now - createdMs > ttlSeconds * 1000) {
272
+ continue;
273
+ }
274
+
275
+ const score = querySimilarity(queryTokens, Array.isArray(entry.tokens) ? entry.tokens : []);
276
+ if (score >= threshold) {
277
+ return {
278
+ score,
279
+ entry,
280
+ };
281
+ }
282
+ }
283
+
284
+ return null;
285
+ }
286
+
287
+ function recordProvenance(event) {
288
+ ensureContextFs();
289
+ const payload = {
290
+ id: `prov_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
291
+ timestamp: nowIso(),
292
+ ...event,
293
+ };
294
+ appendJsonl(path.join(CONTEXTFS_ROOT, NAMESPACES.provenance, 'events.jsonl'), payload);
295
+ return payload;
296
+ }
297
+
298
+ function writeContextObject({ namespace, title, content, tags = [], source, ttl = null, metadata = {} }) {
299
+ ensureContextFs();
300
+
301
+ const id = `${Date.now()}_${toSlug(title)}`;
302
+ const filePath = path.join(CONTEXTFS_ROOT, namespace, `${id}.json`);
303
+
304
+ const doc = {
305
+ id,
306
+ namespace,
307
+ title,
308
+ content,
309
+ tags,
310
+ source: source || 'unknown',
311
+ ttl,
312
+ metadata,
313
+ createdAt: nowIso(),
314
+ lastUsedAt: null,
315
+ };
316
+
317
+ writeJson(filePath, doc);
318
+ indexContextObject(doc, filePath);
319
+
320
+ recordProvenance({
321
+ type: 'context_object_created',
322
+ namespace,
323
+ objectId: id,
324
+ source: doc.source,
325
+ });
326
+
327
+ return {
328
+ id,
329
+ namespace,
330
+ filePath,
331
+ document: doc,
332
+ };
333
+ }
334
+
335
+ function mergeMetadata(existingMetadata = {}, incomingMetadata = {}) {
336
+ const merged = { ...existingMetadata };
337
+
338
+ Object.entries(incomingMetadata).forEach(([key, value]) => {
339
+ if (value === undefined) return;
340
+
341
+ const currentValue = merged[key];
342
+ if (Array.isArray(currentValue) && Array.isArray(value)) {
343
+ merged[key] = [...new Set([...currentValue, ...value])];
344
+ return;
345
+ }
346
+
347
+ merged[key] = value;
348
+ });
349
+
350
+ return merged;
351
+ }
352
+
353
+ function normalizeTagList(tags) {
354
+ return Array.isArray(tags)
355
+ ? [...new Set(tags.map((tag) => String(tag)))]
356
+ .sort()
357
+ : [];
358
+ }
359
+
360
+ function findExistingContextObject({ namespace, title, content, tags = [], source }) {
361
+ ensureContextFs();
362
+
363
+ const expectedTags = normalizeTagList(tags);
364
+ const dirPath = path.join(CONTEXTFS_ROOT, namespace);
365
+ const files = listJsonFiles(dirPath).sort();
366
+
367
+ for (const filePath of files) {
368
+ try {
369
+ const doc = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
370
+ if (doc.title !== title || doc.content !== content || doc.source !== source) {
371
+ continue;
372
+ }
373
+
374
+ if (JSON.stringify(normalizeTagList(doc.tags)) !== JSON.stringify(expectedTags)) {
375
+ continue;
376
+ }
377
+
378
+ return {
379
+ filePath,
380
+ document: doc,
381
+ };
382
+ } catch {
383
+ // Ignore malformed entries while searching for exact duplicates.
384
+ }
385
+ }
386
+
387
+ return null;
388
+ }
389
+
390
+ function upsertContextObject({ namespace, title, content, tags = [], source, ttl = null, metadata = {} }) {
391
+ const existing = findExistingContextObject({
392
+ namespace,
393
+ title,
394
+ content,
395
+ tags,
396
+ source,
397
+ });
398
+
399
+ if (!existing) {
400
+ return writeContextObject({
401
+ namespace,
402
+ title,
403
+ content,
404
+ tags,
405
+ source,
406
+ ttl,
407
+ metadata,
408
+ });
409
+ }
410
+
411
+ const mergedDocument = {
412
+ ...existing.document,
413
+ ttl: ttl === null ? existing.document.ttl || null : ttl,
414
+ metadata: mergeMetadata(existing.document.metadata || {}, metadata),
415
+ };
416
+ writeJson(existing.filePath, mergedDocument);
417
+
418
+ recordProvenance({
419
+ type: 'context_object_deduped',
420
+ namespace,
421
+ objectId: existing.document.id,
422
+ source: source || existing.document.source || 'unknown',
423
+ });
424
+
425
+ return {
426
+ id: mergedDocument.id,
427
+ namespace,
428
+ filePath: existing.filePath,
429
+ document: mergedDocument,
430
+ deduped: true,
431
+ };
432
+ }
433
+
434
+ function registerFeedback(feedbackEvent, memoryRecord = null) {
435
+ ensureContextFs();
436
+
437
+ const raw = writeContextObject({
438
+ namespace: NAMESPACES.rawHistory,
439
+ title: `feedback_${feedbackEvent.signal}_${feedbackEvent.id}`,
440
+ content: JSON.stringify(feedbackEvent),
441
+ tags: feedbackEvent.tags || [],
442
+ source: 'feedback-event',
443
+ metadata: {
444
+ signal: feedbackEvent.signal,
445
+ actionType: feedbackEvent.actionType,
446
+ },
447
+ });
448
+
449
+ let memory = null;
450
+ if (memoryRecord) {
451
+ const namespace = memoryRecord.category === 'error'
452
+ ? NAMESPACES.memoryError
453
+ : NAMESPACES.memoryLearning;
454
+ memory = upsertContextObject({
455
+ namespace,
456
+ title: memoryRecord.title,
457
+ content: memoryRecord.content,
458
+ tags: memoryRecord.tags || [],
459
+ source: 'feedback-memory',
460
+ metadata: {
461
+ category: memoryRecord.category,
462
+ sourceFeedbackId: memoryRecord.sourceFeedbackId,
463
+ },
464
+ });
465
+ }
466
+
467
+ return { raw, memory };
468
+ }
469
+
470
+ function registerPreventionRules(markdown, metadata = {}) {
471
+ return writeContextObject({
472
+ namespace: NAMESPACES.rules,
473
+ title: `prevention_rules_${new Date().toISOString().slice(0, 10)}`,
474
+ content: markdown,
475
+ tags: ['rules', 'prevention'],
476
+ source: 'feedback-loop',
477
+ metadata,
478
+ });
479
+ }
480
+
481
+ function normalizeNamespaces(namespaces) {
482
+ if (!Array.isArray(namespaces) || namespaces.length === 0) {
483
+ return [...DEFAULT_SEARCH_NAMESPACES];
484
+ }
485
+
486
+ const normalized = [];
487
+ namespaces.forEach((rawValue) => {
488
+ const value = String(rawValue || '').trim();
489
+ const mapped = NAMESPACE_ALIAS_MAP.get(value);
490
+ if (!mapped) {
491
+ const err = new Error(`Unsupported namespace: ${value}`);
492
+ err.code = 'INVALID_NAMESPACE';
493
+ throw err;
494
+ }
495
+ if (!normalized.includes(mapped)) {
496
+ normalized.push(mapped);
497
+ }
498
+ });
499
+
500
+ return normalized;
501
+ }
502
+
503
+ function loadCandidates(namespaces) {
504
+ ensureContextFs();
505
+ const selected = normalizeNamespaces(namespaces);
506
+
507
+ const docs = [];
508
+
509
+ selected.forEach((namespace) => {
510
+ const dir = path.join(CONTEXTFS_ROOT, namespace);
511
+ const files = listJsonFiles(dir);
512
+ files.forEach((filePath) => {
513
+ try {
514
+ const payload = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
515
+ docs.push({
516
+ ...payload,
517
+ namespace,
518
+ });
519
+ } catch {
520
+ // ignore malformed files
521
+ }
522
+ });
523
+ });
524
+
525
+ return docs;
526
+ }
527
+
528
+ function scoreDocument(doc, queryTokens) {
529
+ let score = 0;
530
+
531
+ const haystack = `${doc.title || ''} ${doc.content || ''} ${(doc.tags || []).join(' ')}`.toLowerCase();
532
+
533
+ queryTokens.forEach((token) => {
534
+ if (token.length > 2 && haystack.includes(token)) {
535
+ score += 3;
536
+ }
537
+ });
538
+
539
+ if (doc.namespace.includes('memory/error')) score += 1;
540
+ if (doc.namespace.includes('memory/learning')) score += 1;
541
+
542
+ if (doc.createdAt) {
543
+ const ageMs = Date.now() - new Date(doc.createdAt).getTime();
544
+ if (Number.isFinite(ageMs)) {
545
+ const hours = ageMs / (1000 * 60 * 60);
546
+ if (hours < 24) score += 2;
547
+ else if (hours < 24 * 7) score += 1;
548
+ }
549
+ }
550
+
551
+ return score;
552
+ }
553
+
554
+ function buildStructuredContext(doc) {
555
+ const structuredContext = {
556
+ rawContent: doc.content || '',
557
+ reasoning: null,
558
+ whatWentWrong: null,
559
+ whatToChange: null,
560
+ rubricFailure: null,
561
+ };
562
+
563
+ const lines = (doc.content || '').split('\n');
564
+ for (const line of lines) {
565
+ if (line.startsWith('Reasoning:')) structuredContext.reasoning = line.replace('Reasoning:', '').trim();
566
+ else if (line.startsWith('What went wrong:')) structuredContext.whatWentWrong = line.replace('What went wrong:', '').trim();
567
+ else if (line.startsWith('How to avoid:')) structuredContext.whatToChange = line.replace('How to avoid:', '').trim();
568
+ else if (line.startsWith('Rubric failing criteria:')) structuredContext.rubricFailure = line.replace('Rubric failing criteria:', '').trim();
569
+ }
570
+
571
+ return structuredContext;
572
+ }
573
+
574
+ function measureDocumentChars(doc) {
575
+ return `${doc.title || ''}\n${doc.content || ''}`.length;
576
+ }
577
+
578
+ function selectFlatContextItems(candidates, maxItems, maxChars) {
579
+ const selected = [];
580
+ let usedChars = 0;
581
+ let skippedByMaxChars = 0;
582
+
583
+ for (const item of candidates) {
584
+ if (selected.length >= maxItems) break;
585
+
586
+ const snippetLength = measureDocumentChars(item.doc);
587
+ if (usedChars + snippetLength > maxChars) {
588
+ skippedByMaxChars += 1;
589
+ continue;
590
+ }
591
+
592
+ selected.push({
593
+ id: item.doc.id,
594
+ namespace: item.doc.namespace,
595
+ title: item.doc.title,
596
+ structuredContext: buildStructuredContext(item.doc),
597
+ tags: item.doc.tags || [],
598
+ score: item.score,
599
+ });
600
+ usedChars += snippetLength;
601
+ }
602
+
603
+ return {
604
+ items: selected,
605
+ usedChars,
606
+ skippedByMaxChars,
607
+ retrieval: {
608
+ strategy: 'flat',
609
+ themeCount: 0,
610
+ semanticCount: 0,
611
+ selectedThemes: [],
612
+ selectedSemanticGroups: [],
613
+ representativeCount: selected.length,
614
+ expandedEpisodes: 0,
615
+ queryCoverage: null,
616
+ initialCoverage: null,
617
+ coverageTarget: null,
618
+ },
619
+ };
620
+ }
621
+
622
+ /* ── Memex-style Indexed Memory ────────────────────────────────── */
623
+
624
+ const MEMEX_INDEX_FILE = 'memex-index.jsonl';
625
+
626
+ function getMemexIndexPath() {
627
+ return path.join(CONTEXTFS_ROOT, NAMESPACES.provenance, MEMEX_INDEX_FILE);
628
+ }
629
+
630
+ function buildIndexEntry(doc, filePath) {
631
+ const resolvedNamespace = doc.namespace || inferNamespaceFromPath(filePath);
632
+ return {
633
+ id: doc.id,
634
+ namespace: resolvedNamespace,
635
+ title: doc.title || '',
636
+ tags: doc.tags || [],
637
+ digest: String(doc.content || '').slice(0, 120),
638
+ createdAt: doc.createdAt || nowIso(),
639
+ stableRef: filePath,
640
+ };
641
+ }
642
+
643
+ function indexContextObject(doc, filePath) {
644
+ const entry = buildIndexEntry(doc, filePath);
645
+ appendJsonl(getMemexIndexPath(), entry);
646
+ return entry;
647
+ }
648
+
649
+ function loadMemexIndex() {
650
+ return readJsonl(getMemexIndexPath());
651
+ }
652
+
653
+ function dereferenceEntry(entry) {
654
+ if (!entry || !entry.stableRef) return null;
655
+ try {
656
+ return JSON.parse(fs.readFileSync(entry.stableRef, 'utf-8'));
657
+ } catch {
658
+ return null;
659
+ }
660
+ }
661
+
662
+ function searchMemexIndex({ query = '', maxResults = 10, namespaces = [] } = {}) {
663
+ const index = loadMemexIndex();
664
+ const tokens = tokenizeQuery(query);
665
+ const nsFilter = namespaces.length > 0 ? new Set(normalizeNamespaces(namespaces)) : null;
666
+
667
+ const scored = index
668
+ .filter((entry) => {
669
+ const entryNamespace = entry.namespace || inferNamespaceFromPath(entry.stableRef);
670
+ return !nsFilter || nsFilter.has(entryNamespace);
671
+ })
672
+ .map((entry) => {
673
+ const entryNamespace = entry.namespace || inferNamespaceFromPath(entry.stableRef);
674
+ const haystack = `${entry.title} ${entry.digest} ${(entry.tags || []).join(' ')}`.toLowerCase();
675
+ let score = 0;
676
+ tokens.forEach((t) => { if (t.length > 2 && haystack.includes(t)) score += 3; });
677
+ if (entryNamespace.includes('memory/error')) score += 1;
678
+ if (entryNamespace.includes('memory/learning')) score += 1;
679
+ if (entry.createdAt) {
680
+ const hours = (Date.now() - new Date(entry.createdAt).getTime()) / 3_600_000;
681
+ if (Number.isFinite(hours)) {
682
+ if (hours < 24) score += 2;
683
+ else if (hours < 168) score += 1;
684
+ }
685
+ }
686
+ return { entry: { ...entry, namespace: entryNamespace }, score };
687
+ })
688
+ .filter((x) => x.score > 0)
689
+ .sort((a, b) => b.score - a.score)
690
+ .slice(0, maxResults);
691
+
692
+ return scored.map((x) => ({ ...x.entry, _score: x.score }));
693
+ }
694
+
695
+ function constructMemexPack({ query = '', maxItems = 8, maxChars = 6000, namespaces = [] } = {}) {
696
+ const normalizedNamespaces = normalizeNamespaces(namespaces);
697
+ const hits = searchMemexIndex({ query, maxResults: maxItems * 2, namespaces: normalizedNamespaces });
698
+
699
+ const items = [];
700
+ let usedChars = 0;
701
+ const dereferenced = [];
702
+ let skippedByMaxChars = 0;
703
+
704
+ for (const hit of hits) {
705
+ if (items.length >= maxItems) break;
706
+ const full = dereferenceEntry(hit);
707
+ if (!full) continue;
708
+
709
+ const snippetLength = measureDocumentChars(full);
710
+ if (usedChars + snippetLength > maxChars) {
711
+ skippedByMaxChars += 1;
712
+ continue;
713
+ }
714
+
715
+ items.push({
716
+ id: full.id,
717
+ namespace: hit.namespace,
718
+ title: full.title,
719
+ structuredContext: buildStructuredContext(full),
720
+ tags: full.tags || [],
721
+ score: hit._score,
722
+ });
723
+ usedChars += snippetLength;
724
+ dereferenced.push(hit.id);
725
+ }
726
+
727
+ const visibility = {
728
+ itemCount: items.length,
729
+ sourceCandidateCount: hits.length,
730
+ hiddenCount: Math.max(hits.length - items.length, 0),
731
+ maxItemsHit: hits.length > maxItems && items.length >= maxItems,
732
+ maxCharsHit: skippedByMaxChars > 0,
733
+ skippedByMaxChars,
734
+ remainingCharBudget: Math.max(maxChars - usedChars, 0),
735
+ visibleTitles: items.slice(0, 5).map((item) => item.title),
736
+ };
737
+
738
+ const packId = `memex_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
739
+ const pack = {
740
+ packId,
741
+ query,
742
+ maxItems,
743
+ maxChars,
744
+ usedChars,
745
+ namespaces: normalizedNamespaces,
746
+ createdAt: nowIso(),
747
+ items,
748
+ indexHits: hits.length,
749
+ dereferencedCount: dereferenced.length,
750
+ visibility,
751
+ cache: { hit: false },
752
+ };
753
+
754
+ appendJsonl(path.join(CONTEXTFS_ROOT, NAMESPACES.provenance, 'packs.jsonl'), pack);
755
+ recordProvenance({
756
+ type: 'memex_pack_constructed',
757
+ packId,
758
+ query,
759
+ indexHits: hits.length,
760
+ dereferencedCount: dereferenced.length,
761
+ usedChars,
762
+ });
763
+
764
+ return pack;
765
+ }
766
+
767
+ function constructContextPack({ query = '', maxItems = 8, maxChars = 6000, namespaces = [] } = {}) {
768
+ const normalizedNamespaces = normalizeNamespaces(namespaces);
769
+ const tokens = tokenizeQuery(query);
770
+ const sourceHash = getSourceHash(normalizedNamespaces);
771
+
772
+ const cacheHit = findSemanticCacheHit({
773
+ query,
774
+ namespaces: normalizedNamespaces,
775
+ maxItems,
776
+ maxChars,
777
+ });
778
+
779
+ if (cacheHit) {
780
+ const packId = `pack_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
781
+ const cachedPack = cacheHit.entry.pack;
782
+ const pack = {
783
+ ...cachedPack,
784
+ packId,
785
+ query,
786
+ createdAt: nowIso(),
787
+ cache: {
788
+ hit: true,
789
+ similarity: Number(cacheHit.score.toFixed(4)),
790
+ matchedQuery: cacheHit.entry.query,
791
+ sourcePackId: cachedPack.packId,
792
+ },
793
+ };
794
+
795
+ appendJsonl(path.join(CONTEXTFS_ROOT, NAMESPACES.provenance, 'packs.jsonl'), pack);
796
+ recordProvenance({
797
+ type: 'context_pack_cache_hit',
798
+ packId,
799
+ sourcePackId: cachedPack.packId,
800
+ query,
801
+ similarity: Number(cacheHit.score.toFixed(4)),
802
+ itemCount: Array.isArray(pack.items) ? pack.items.length : 0,
803
+ });
804
+
805
+ return pack;
806
+ }
807
+
808
+ const candidates = loadCandidates(normalizedNamespaces)
809
+ .map((doc) => ({ doc, score: scoreDocument(doc, tokens) }))
810
+ .sort((a, b) => b.score - a.score);
811
+
812
+ const hierarchicalRetrievalEnabled = shouldUseHierarchicalRetrieval(normalizedNamespaces);
813
+ const selection = hierarchicalRetrievalEnabled
814
+ ? retrieveHierarchicalDocuments({
815
+ documents: candidates.map((candidate) => candidate.doc),
816
+ query,
817
+ maxItems,
818
+ maxChars,
819
+ scorer: scoreDocument,
820
+ measureDocument: measureDocumentChars,
821
+ })
822
+ : selectFlatContextItems(candidates, maxItems, maxChars);
823
+
824
+ const selected = selection.items.map((doc) => ({
825
+ id: doc.id,
826
+ namespace: doc.namespace,
827
+ title: doc.title,
828
+ structuredContext: buildStructuredContext(doc),
829
+ tags: doc.tags || [],
830
+ score: scoreDocument(doc, tokens),
831
+ }));
832
+ const usedChars = selection.usedChars;
833
+ const skippedByMaxChars = selection.skippedByMaxChars;
834
+
835
+ const visibility = {
836
+ itemCount: selected.length,
837
+ sourceCandidateCount: candidates.length,
838
+ hiddenCount: Math.max(candidates.length - selected.length, 0),
839
+ maxItemsHit: candidates.length > maxItems && selected.length >= maxItems,
840
+ maxCharsHit: skippedByMaxChars > 0,
841
+ skippedByMaxChars,
842
+ remainingCharBudget: Math.max(maxChars - usedChars, 0),
843
+ visibleTitles: selected.slice(0, 5).map((item) => item.title),
844
+ };
845
+
846
+ const packId = `pack_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
847
+ const pack = {
848
+ packId,
849
+ query,
850
+ maxItems,
851
+ maxChars,
852
+ usedChars,
853
+ namespaces: normalizedNamespaces,
854
+ createdAt: nowIso(),
855
+ items: selected,
856
+ visibility,
857
+ cache: {
858
+ hit: false,
859
+ },
860
+ sourceHash,
861
+ retrieval: selection.retrieval,
862
+ };
863
+
864
+ appendJsonl(path.join(CONTEXTFS_ROOT, NAMESPACES.provenance, 'packs.jsonl'), pack);
865
+ appendSemanticCacheEntry({
866
+ id: `cache_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
867
+ timestamp: nowIso(),
868
+ key: buildSemanticCacheKey({
869
+ namespaces: normalizedNamespaces,
870
+ maxItems,
871
+ maxChars,
872
+ }),
873
+ query,
874
+ tokens,
875
+ sourceHash,
876
+ pack,
877
+ });
878
+ recordProvenance({
879
+ type: 'context_pack_constructed',
880
+ packId,
881
+ query,
882
+ itemCount: selected.length,
883
+ usedChars,
884
+ sourceHash,
885
+ retrievalStrategy: selection.retrieval.strategy,
886
+ });
887
+
888
+ return pack;
889
+ }
890
+
891
+ function evaluateContextPack({ packId, outcome, signal = null, notes = '', rubricEvaluation = null }) {
892
+ const evaluation = {
893
+ id: `eval_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
894
+ packId,
895
+ outcome,
896
+ signal,
897
+ notes,
898
+ rubricEvaluation,
899
+ timestamp: nowIso(),
900
+ };
901
+
902
+ appendJsonl(path.join(CONTEXTFS_ROOT, NAMESPACES.provenance, 'evaluations.jsonl'), evaluation);
903
+ recordProvenance({
904
+ type: 'context_pack_evaluated',
905
+ packId,
906
+ outcome,
907
+ signal,
908
+ rubricPromotionEligible: rubricEvaluation ? rubricEvaluation.promotionEligible : null,
909
+ });
910
+
911
+ return evaluation;
912
+ }
913
+
914
+ function getProvenance(limit = 50) {
915
+ const eventsPath = path.join(CONTEXTFS_ROOT, NAMESPACES.provenance, 'events.jsonl');
916
+ const events = readJsonl(eventsPath);
917
+ return events.slice(-limit);
918
+ }
919
+
920
+ /**
921
+ * Write a session handoff primer to contextfs/session/primer.json.
922
+ * Captures git state, last task, next step, and blockers so the next
923
+ * session starts with full context — no manual primer.md needed.
924
+ */
925
+ function writeSessionHandoff({ project, branch, lastTask, nextStep, blockers, openFiles, customContext } = {}) {
926
+ ensureDir(path.join(CONTEXTFS_ROOT, NAMESPACES.session));
927
+
928
+ let gitContext = {};
929
+ try {
930
+ const { execSync } = require('child_process');
931
+ const cwd = process.cwd();
932
+ gitContext = {
933
+ branch: branch || execSync('git rev-parse --abbrev-ref HEAD', { cwd, encoding: 'utf8' }).trim(),
934
+ lastCommits: execSync('git log --oneline -5', { cwd, encoding: 'utf8' }).trim().split('\n'),
935
+ modifiedFiles: execSync('git diff --name-only HEAD~1 2>/dev/null || echo ""', { cwd, encoding: 'utf8' }).trim().split('\n').filter(Boolean),
936
+ status: execSync('git status --short', { cwd, encoding: 'utf8' }).trim().split('\n').filter(Boolean),
937
+ };
938
+ } catch (_) {
939
+ gitContext = { branch: branch || 'unknown', lastCommits: [], modifiedFiles: [], status: [] };
940
+ }
941
+
942
+ const primer = {
943
+ id: `session_${Date.now()}_${crypto.randomBytes(3).toString('hex')}`,
944
+ timestamp: nowIso(),
945
+ project: project || path.basename(process.cwd()),
946
+ git: gitContext,
947
+ lastTask: lastTask || null,
948
+ nextStep: nextStep || null,
949
+ blockers: Array.isArray(blockers) ? blockers : (blockers ? [blockers] : []),
950
+ openFiles: Array.isArray(openFiles) ? openFiles : [],
951
+ customContext: customContext || null,
952
+ };
953
+
954
+ const primerPath = path.join(CONTEXTFS_ROOT, NAMESPACES.session, 'primer.json');
955
+ fs.writeFileSync(primerPath, JSON.stringify(primer, null, 2));
956
+
957
+ // Sync to primer.md if it exists
958
+ const mdPrimerPath = path.join(process.cwd(), 'primer.md');
959
+ if (fs.existsSync(mdPrimerPath)) {
960
+ try {
961
+ let md = fs.readFileSync(mdPrimerPath, 'utf8');
962
+ if (primer.lastTask) {
963
+ md = md.replace(/## Last Completed Task\n- .*/, `## Last Completed Task\n- ${primer.lastTask}`);
964
+ }
965
+ if (primer.nextStep) {
966
+ md = md.replace(/## Exact Next Step\n- .*/, `## Exact Next Step\n- ${primer.nextStep}`);
967
+ }
968
+ if (primer.blockers.length > 0) {
969
+ md = md.replace(/## Open Blockers\n(?:- .*\n)*/, `## Open Blockers\n${primer.blockers.map(b => `- ${b}`).join('\n')}\n`);
970
+ }
971
+ fs.writeFileSync(mdPrimerPath, md);
972
+
973
+ // Trigger full memory refresh (Layer 3, 4, 5)
974
+ const { execSync } = require('child_process');
975
+ execSync('./bin/memory.sh', { stdio: 'ignore' });
976
+ } catch (e) {
977
+ console.error('Warning: Failed to sync to primer.md:', e.message);
978
+ }
979
+ }
980
+
981
+ recordProvenance({
982
+ action: 'session_handoff',
983
+ source: 'session',
984
+ detail: `Handoff: ${primer.lastTask || 'no task'} → ${primer.nextStep || 'no next step'}`,
985
+ });
986
+
987
+ return primer;
988
+ }
989
+
990
+ /**
991
+ * Read the most recent session handoff primer.
992
+ */
993
+ function readSessionHandoff() {
994
+ const primerPath = path.join(CONTEXTFS_ROOT, NAMESPACES.session, 'primer.json');
995
+ if (!fs.existsSync(primerPath)) return null;
996
+ try {
997
+ return JSON.parse(fs.readFileSync(primerPath, 'utf8'));
998
+ } catch (_) {
999
+ return null;
1000
+ }
1001
+ }
1002
+
1003
+ // ---------------------------------------------------------------------------
1004
+ // Multi-Hop Agentic Retrieval (Context-1 inspired)
1005
+ // ---------------------------------------------------------------------------
1006
+
1007
+ /** Default max retrieval hops before stopping. */
1008
+ const MAX_HOPS = 3;
1009
+ /** Minimum coverage score (0–1) to stop early. */
1010
+ const COVERAGE_THRESHOLD = 0.6;
1011
+ /** Minimum relevance score for an item to survive pruning. */
1012
+ const PRUNE_SCORE_FLOOR = 2;
1013
+
1014
+ /**
1015
+ * Compute query coverage: what fraction of query tokens appear in the
1016
+ * assembled context items. Returns 0–1.
1017
+ */
1018
+ function computeCoverage(queryTokens, items) {
1019
+ if (queryTokens.length === 0) return 1;
1020
+ const contextText = items.map((i) =>
1021
+ `${i.title || ''} ${i.structuredContext ? i.structuredContext.rawContent || '' : ''} ${(i.tags || []).join(' ')}`
1022
+ ).join(' ').toLowerCase();
1023
+ const contextTokens = new Set(contextText.split(/[^a-z0-9]+/).filter(Boolean));
1024
+ let covered = 0;
1025
+ for (const token of queryTokens) {
1026
+ if (token.length > 2 && contextTokens.has(token)) covered++;
1027
+ }
1028
+ return covered / queryTokens.length;
1029
+ }
1030
+
1031
+ /**
1032
+ * Prune weak chunks: re-score all items against query and drop any below
1033
+ * PRUNE_SCORE_FLOOR. Returns the survivors sorted by score descending.
1034
+ */
1035
+ function pruneWeakChunks(items, queryTokens) {
1036
+ return items
1037
+ .map((item) => {
1038
+ const haystack = `${item.title || ''} ${item.structuredContext ? item.structuredContext.rawContent || '' : ''} ${(item.tags || []).join(' ')}`.toLowerCase();
1039
+ let score = 0;
1040
+ for (const token of queryTokens) {
1041
+ if (token.length > 2 && haystack.includes(token)) score += 3;
1042
+ }
1043
+ if (item.namespace && item.namespace.includes('memory/')) score += 1;
1044
+ return { item, score };
1045
+ })
1046
+ .filter((x) => x.score >= PRUNE_SCORE_FLOOR)
1047
+ .sort((a, b) => b.score - a.score)
1048
+ .map((x) => ({ ...x.item, score: x.score }));
1049
+ }
1050
+
1051
+ /**
1052
+ * Refine a query based on what's already been retrieved — extract tokens
1053
+ * from retrieved items that weren't in the original query (expansion),
1054
+ * then combine with original tokens for the next hop.
1055
+ */
1056
+ function refineQuery(originalTokens, items) {
1057
+ const original = new Set(originalTokens);
1058
+ const expansion = new Set();
1059
+ for (const item of items) {
1060
+ const itemTokens = tokenizeQuery(
1061
+ `${item.title || ''} ${(item.tags || []).join(' ')}`
1062
+ );
1063
+ for (const t of itemTokens) {
1064
+ if (t.length > 3 && !original.has(t)) expansion.add(t);
1065
+ }
1066
+ }
1067
+ // Take top 3 expansion terms (by length, as a proxy for specificity)
1068
+ const sorted = [...expansion].sort((a, b) => b.length - a.length).slice(0, 3);
1069
+ return [...originalTokens, ...sorted];
1070
+ }
1071
+
1072
+ /**
1073
+ * Multi-hop context pack construction. Iteratively retrieves, prunes weak
1074
+ * chunks, checks coverage, and refines the query until coverage is
1075
+ * sufficient or max hops are reached.
1076
+ *
1077
+ * @param {Object} opts
1078
+ * @param {string} opts.query - Search query
1079
+ * @param {number} [opts.maxItems=8] - Max items in final pack
1080
+ * @param {number} [opts.maxChars=6000] - Max chars budget
1081
+ * @param {string[]} [opts.namespaces=[]] - Namespaces to search
1082
+ * @param {number} [opts.maxHops=MAX_HOPS] - Max retrieval iterations
1083
+ * @returns {Object} Context pack with hop metadata
1084
+ */
1085
+ function constructMultiHopPack({ query = '', maxItems = 8, maxChars = 6000, namespaces = [], maxHops = MAX_HOPS } = {}) {
1086
+ const normalizedNamespaces = normalizeNamespaces(namespaces);
1087
+ const originalTokens = tokenizeQuery(query);
1088
+ const sourceHash = getSourceHash(normalizedNamespaces);
1089
+
1090
+ // Check cache first (same as single-hop)
1091
+ const cacheHit = findSemanticCacheHit({ query, namespaces: normalizedNamespaces, maxItems, maxChars });
1092
+ if (cacheHit) {
1093
+ const packId = `mhop_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
1094
+ return { ...cacheHit.entry.pack, packId, query, createdAt: nowIso(), cache: { hit: true, similarity: Number(cacheHit.score.toFixed(4)), sourcePackId: cacheHit.entry.pack.packId } };
1095
+ }
1096
+
1097
+ const allCandidates = loadCandidates(normalizedNamespaces);
1098
+ let currentTokens = [...originalTokens];
1099
+ let accumulatedItems = [];
1100
+ let usedChars = 0;
1101
+ const hopLog = [];
1102
+ const seenIds = new Set();
1103
+
1104
+ for (let hop = 0; hop < maxHops; hop++) {
1105
+ // Score candidates with current (possibly refined) query
1106
+ const scored = allCandidates
1107
+ .filter((doc) => !seenIds.has(doc.id))
1108
+ .map((doc) => ({ doc, score: scoreDocument(doc, currentTokens) }))
1109
+ .filter((x) => x.score > 0)
1110
+ .sort((a, b) => b.score - a.score);
1111
+
1112
+ // Take top candidates that fit within budget
1113
+ const hopItems = [];
1114
+ for (const { doc, score } of scored) {
1115
+ if (accumulatedItems.length + hopItems.length >= maxItems) break;
1116
+ const snippetLen = measureDocumentChars(doc);
1117
+ if (usedChars + snippetLen > maxChars) continue;
1118
+
1119
+ const item = {
1120
+ id: doc.id,
1121
+ namespace: doc.namespace,
1122
+ title: doc.title,
1123
+ structuredContext: buildStructuredContext(doc),
1124
+ tags: doc.tags || [],
1125
+ score,
1126
+ };
1127
+ hopItems.push(item);
1128
+ usedChars += snippetLen;
1129
+ seenIds.add(doc.id);
1130
+ }
1131
+
1132
+ accumulatedItems.push(...hopItems);
1133
+
1134
+ // Prune weak chunks from the accumulated set
1135
+ const beforePrune = accumulatedItems.length;
1136
+ accumulatedItems = pruneWeakChunks(accumulatedItems, originalTokens);
1137
+ const pruned = beforePrune - accumulatedItems.length;
1138
+
1139
+ // Recalculate usedChars after pruning
1140
+ usedChars = accumulatedItems.reduce((sum, item) => {
1141
+ const content = item.structuredContext ? item.structuredContext.rawContent || '' : '';
1142
+ return sum + `${item.title || ''}\n${content}`.length;
1143
+ }, 0);
1144
+
1145
+ // Check coverage
1146
+ const coverage = computeCoverage(originalTokens, accumulatedItems);
1147
+
1148
+ hopLog.push({
1149
+ hop: hop + 1,
1150
+ newItems: hopItems.length,
1151
+ prunedItems: pruned,
1152
+ totalItems: accumulatedItems.length,
1153
+ coverage: Number(coverage.toFixed(3)),
1154
+ queryTokenCount: currentTokens.length,
1155
+ });
1156
+
1157
+ // Stop if coverage is good or we have enough items
1158
+ if (coverage >= COVERAGE_THRESHOLD || accumulatedItems.length >= maxItems) break;
1159
+
1160
+ // Refine query for next hop
1161
+ currentTokens = refineQuery(originalTokens, accumulatedItems);
1162
+ }
1163
+
1164
+ const visibility = {
1165
+ itemCount: accumulatedItems.length,
1166
+ sourceCandidateCount: allCandidates.length,
1167
+ hiddenCount: Math.max(allCandidates.length - accumulatedItems.length, 0),
1168
+ maxItemsHit: accumulatedItems.length >= maxItems,
1169
+ maxCharsHit: usedChars >= maxChars,
1170
+ remainingCharBudget: Math.max(maxChars - usedChars, 0),
1171
+ visibleTitles: accumulatedItems.slice(0, 5).map((i) => i.title),
1172
+ };
1173
+
1174
+ const packId = `mhop_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
1175
+ const pack = {
1176
+ packId,
1177
+ query,
1178
+ maxItems,
1179
+ maxChars,
1180
+ usedChars,
1181
+ namespaces: normalizedNamespaces,
1182
+ createdAt: nowIso(),
1183
+ items: accumulatedItems,
1184
+ visibility,
1185
+ cache: { hit: false },
1186
+ sourceHash,
1187
+ retrieval: {
1188
+ strategy: 'multi-hop',
1189
+ hops: hopLog,
1190
+ totalHops: hopLog.length,
1191
+ finalCoverage: hopLog.length > 0 ? hopLog[hopLog.length - 1].coverage : 0,
1192
+ },
1193
+ };
1194
+
1195
+ appendJsonl(path.join(CONTEXTFS_ROOT, NAMESPACES.provenance, 'packs.jsonl'), pack);
1196
+ appendSemanticCacheEntry({
1197
+ id: `cache_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
1198
+ timestamp: nowIso(),
1199
+ key: buildSemanticCacheKey({ namespaces: normalizedNamespaces, maxItems, maxChars }),
1200
+ query,
1201
+ tokens: originalTokens,
1202
+ sourceHash,
1203
+ pack,
1204
+ });
1205
+ recordProvenance({
1206
+ type: 'multi_hop_pack_constructed',
1207
+ packId,
1208
+ query,
1209
+ itemCount: accumulatedItems.length,
1210
+ hops: hopLog.length,
1211
+ finalCoverage: pack.retrieval.finalCoverage,
1212
+ usedChars,
1213
+ sourceHash,
1214
+ });
1215
+
1216
+ return pack;
1217
+ }
1218
+
1219
+ function constructTemplatedPack({ template, query } = {}) {
1220
+ const config = PACK_TEMPLATES[template];
1221
+ if (!config) {
1222
+ throw new Error(`Unknown pack template: "${template}". Available: ${Object.keys(PACK_TEMPLATES).join(', ')}`);
1223
+ }
1224
+
1225
+ const fullQuery = `${config.queryPrefix} ${query || ''}`.trim();
1226
+ const pack = constructContextPack({
1227
+ query: fullQuery,
1228
+ namespaces: config.namespaces,
1229
+ maxItems: config.maxItems,
1230
+ maxChars: config.maxChars,
1231
+ });
1232
+ pack.template = template;
1233
+ return pack;
1234
+ }
1235
+
1236
+ function listPackTemplates() {
1237
+ return Object.entries(PACK_TEMPLATES).map(([name, config]) => ({
1238
+ name,
1239
+ namespaces: config.namespaces,
1240
+ maxItems: config.maxItems,
1241
+ maxChars: config.maxChars,
1242
+ queryPrefix: config.queryPrefix,
1243
+ }));
1244
+ }
1245
+
1246
+ module.exports = {
1247
+ CONTEXTFS_ROOT,
1248
+ NAMESPACES,
1249
+ ensureContextFs,
1250
+ recordProvenance,
1251
+ writeContextObject,
1252
+ upsertContextObject,
1253
+ registerFeedback,
1254
+ registerPreventionRules,
1255
+ normalizeNamespaces,
1256
+ constructContextPack,
1257
+ evaluateContextPack,
1258
+ getProvenance,
1259
+ readJsonl,
1260
+ DEFAULT_SEARCH_NAMESPACES,
1261
+ tokenizeQuery,
1262
+ querySimilarity,
1263
+ findSemanticCacheHit,
1264
+ getSemanticCacheConfig,
1265
+ buildIndexEntry,
1266
+ loadMemexIndex,
1267
+ dereferenceEntry,
1268
+ searchMemexIndex,
1269
+ constructMemexPack,
1270
+ writeSessionHandoff,
1271
+ readSessionHandoff,
1272
+ PACK_TEMPLATES,
1273
+ constructTemplatedPack,
1274
+ listPackTemplates,
1275
+ constructMultiHopPack,
1276
+ computeCoverage,
1277
+ pruneWeakChunks,
1278
+ refineQuery,
1279
+ MAX_HOPS,
1280
+ COVERAGE_THRESHOLD,
1281
+ PRUNE_SCORE_FLOOR,
1282
+ };
1283
+
1284
+ if (require.main === module) {
1285
+ ensureContextFs();
1286
+ console.log(`ContextFS ready at ${CONTEXTFS_ROOT}`);
1287
+ }