@theokit/sdk 4.56.0 → 4.58.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 (151) hide show
  1. package/CHANGELOG.md +170 -0
  2. package/dist/{agent-K3PQBS7D.js → agent-6WM4QOWS.js} +15 -14
  3. package/dist/{agent-K3PQBS7D.js.map → agent-6WM4QOWS.js.map} +1 -1
  4. package/dist/{agent-PJMBK7HQ.cjs → agent-MIYY7TXV.cjs} +16 -15
  5. package/dist/{agent-PJMBK7HQ.cjs.map → agent-MIYY7TXV.cjs.map} +1 -1
  6. package/dist/agent-session-store-G2BMUZQV.js +7 -0
  7. package/dist/{agent-session-store-P6V3FINW.js.map → agent-session-store-G2BMUZQV.js.map} +1 -1
  8. package/dist/agent-session-store-WAKLOKWK.cjs +24 -0
  9. package/dist/{agent-session-store-ZJ3JSRS4.cjs.map → agent-session-store-WAKLOKWK.cjs.map} +1 -1
  10. package/dist/{chunk-6VLKVX7O.cjs → chunk-27AXGJMV.cjs} +26 -15
  11. package/dist/chunk-27AXGJMV.cjs.map +1 -0
  12. package/dist/{chunk-HLPSLDGL.cjs → chunk-43GFJ5SD.cjs} +7 -7
  13. package/dist/{chunk-HLPSLDGL.cjs.map → chunk-43GFJ5SD.cjs.map} +1 -1
  14. package/dist/{chunk-M7UHYBEY.js → chunk-5QOIQJ5J.js} +3 -3
  15. package/dist/{chunk-M7UHYBEY.js.map → chunk-5QOIQJ5J.js.map} +1 -1
  16. package/dist/{chunk-E3WZ6Y4H.cjs → chunk-7BG5UH3T.cjs} +8 -8
  17. package/dist/{chunk-E3WZ6Y4H.cjs.map → chunk-7BG5UH3T.cjs.map} +1 -1
  18. package/dist/chunk-AQO3NSRG.cjs +41 -0
  19. package/dist/chunk-AQO3NSRG.cjs.map +1 -0
  20. package/dist/{chunk-JHPGF3FP.cjs → chunk-BUIK7GUA.cjs} +22 -2
  21. package/dist/chunk-BUIK7GUA.cjs.map +1 -0
  22. package/dist/{chunk-KBXWEWKS.js → chunk-CF5OMCZG.js} +87 -43
  23. package/dist/chunk-CF5OMCZG.js.map +1 -0
  24. package/dist/{chunk-DZBSJX6J.js → chunk-FKMUFNQE.js} +21 -4
  25. package/dist/chunk-FKMUFNQE.js.map +1 -0
  26. package/dist/{chunk-BMAZLQQ4.js → chunk-G24KZRSA.js} +3 -3
  27. package/dist/{chunk-BMAZLQQ4.js.map → chunk-G24KZRSA.js.map} +1 -1
  28. package/dist/{chunk-MVAPK2BX.js → chunk-IACR5LEM.js} +3 -3
  29. package/dist/{chunk-MVAPK2BX.js.map → chunk-IACR5LEM.js.map} +1 -1
  30. package/dist/{chunk-U2AC6JUP.cjs → chunk-J2UROOIG.cjs} +2 -27
  31. package/dist/chunk-J2UROOIG.cjs.map +1 -0
  32. package/dist/{chunk-V2UUGUY4.cjs → chunk-KKK3FZ4A.cjs} +6 -6
  33. package/dist/{chunk-V2UUGUY4.cjs.map → chunk-KKK3FZ4A.cjs.map} +1 -1
  34. package/dist/{chunk-ZNW6V4Y6.cjs → chunk-LKFET5A3.cjs} +23 -2
  35. package/dist/chunk-LKFET5A3.cjs.map +1 -0
  36. package/dist/{chunk-YMA4S2WO.js → chunk-MZ2FGBLZ.js} +33 -11
  37. package/dist/chunk-MZ2FGBLZ.js.map +1 -0
  38. package/dist/{chunk-LQYQHXIW.cjs → chunk-N47KLTKD.cjs} +163 -119
  39. package/dist/chunk-N47KLTKD.cjs.map +1 -0
  40. package/dist/{chunk-53CBTBWO.cjs → chunk-P6H23T43.cjs} +33 -11
  41. package/dist/chunk-P6H23T43.cjs.map +1 -0
  42. package/dist/{chunk-YPJ5NH5N.cjs → chunk-QI7WE46I.cjs} +29 -6
  43. package/dist/chunk-QI7WE46I.cjs.map +1 -0
  44. package/dist/{chunk-H5GOSXSC.cjs → chunk-QIRDQZYL.cjs} +4 -4
  45. package/dist/{chunk-H5GOSXSC.cjs.map → chunk-QIRDQZYL.cjs.map} +1 -1
  46. package/dist/{chunk-7FHZ4VQX.js → chunk-SAVIWMZB.js} +3 -3
  47. package/dist/{chunk-7FHZ4VQX.js.map → chunk-SAVIWMZB.js.map} +1 -1
  48. package/dist/{chunk-UPRJR6IP.js → chunk-UCBJBJ27.js} +3 -25
  49. package/dist/chunk-UCBJBJ27.js.map +1 -0
  50. package/dist/chunk-UFLD2HEV.js +35 -0
  51. package/dist/chunk-UFLD2HEV.js.map +1 -0
  52. package/dist/{chunk-GTKFV7O5.js → chunk-WFC26L6Y.js} +23 -2
  53. package/dist/chunk-WFC26L6Y.js.map +1 -0
  54. package/dist/{chunk-OHBIUXKM.js → chunk-WG7R5W6R.js} +4 -4
  55. package/dist/{chunk-OHBIUXKM.js.map → chunk-WG7R5W6R.js.map} +1 -1
  56. package/dist/{chunk-ODTZ7HX3.js → chunk-WLZ7XRWH.js} +25 -15
  57. package/dist/chunk-WLZ7XRWH.js.map +1 -0
  58. package/dist/{chunk-HJBMA5MB.cjs → chunk-WNTAPVU5.cjs} +47 -6
  59. package/dist/chunk-WNTAPVU5.cjs.map +1 -0
  60. package/dist/{chunk-GJ6RK75E.js → chunk-YJDH3CKG.js} +29 -6
  61. package/dist/chunk-YJDH3CKG.js.map +1 -0
  62. package/dist/{chunk-CTCGWIUD.cjs → chunk-Z4BCLVTI.cjs} +5 -5
  63. package/dist/{chunk-CTCGWIUD.cjs.map → chunk-Z4BCLVTI.cjs.map} +1 -1
  64. package/dist/{chunk-2QKTVKH3.js → chunk-ZHPWGIUX.js} +48 -8
  65. package/dist/chunk-ZHPWGIUX.js.map +1 -0
  66. package/dist/{compact-session-FUJDVF7B.js → compact-session-QGDNP45U.js} +4 -4
  67. package/dist/{compact-session-FUJDVF7B.js.map → compact-session-QGDNP45U.js.map} +1 -1
  68. package/dist/{compact-session-LEWF554G.cjs → compact-session-YHFQIXV5.cjs} +12 -12
  69. package/dist/{compact-session-LEWF554G.cjs.map → compact-session-YHFQIXV5.cjs.map} +1 -1
  70. package/dist/context/index.cjs +7 -7
  71. package/dist/context/index.js +3 -3
  72. package/dist/{context-VMIE4BMD.cjs → context-LCKNH2XI.cjs} +7 -6
  73. package/dist/{context-VMIE4BMD.cjs.map → context-LCKNH2XI.cjs.map} +1 -1
  74. package/dist/context-MZSKGKYR.js +7 -0
  75. package/dist/{context-FDOON2DB.js.map → context-MZSKGKYR.js.map} +1 -1
  76. package/dist/cron.cjs +15 -14
  77. package/dist/cron.js +14 -13
  78. package/dist/eval.cjs +14 -13
  79. package/dist/eval.cjs.map +1 -1
  80. package/dist/eval.js +13 -12
  81. package/dist/eval.js.map +1 -1
  82. package/dist/{fs-session-store-IN7JTTXD.js → fs-session-store-4BHGIWRE.js} +4 -4
  83. package/dist/{fs-session-store-IN7JTTXD.js.map → fs-session-store-4BHGIWRE.js.map} +1 -1
  84. package/dist/{fs-session-store-3MOJQGP2.cjs → fs-session-store-XZ7ZTB3A.cjs} +5 -5
  85. package/dist/{fs-session-store-3MOJQGP2.cjs.map → fs-session-store-XZ7ZTB3A.cjs.map} +1 -1
  86. package/dist/{index-manager-WC5ZVJWO.cjs → index-manager-J2QIDI6I.cjs} +5 -4
  87. package/dist/{index-manager-WC5ZVJWO.cjs.map → index-manager-J2QIDI6I.cjs.map} +1 -1
  88. package/dist/{index-manager-WBIMDVQT.js → index-manager-WYESAVTX.js} +4 -3
  89. package/dist/{index-manager-WBIMDVQT.js.map → index-manager-WYESAVTX.js.map} +1 -1
  90. package/dist/index.cjs +44 -45
  91. package/dist/index.cjs.map +1 -1
  92. package/dist/index.d.cts +1 -1
  93. package/dist/index.d.ts +1 -1
  94. package/dist/index.js +17 -18
  95. package/dist/index.js.map +1 -1
  96. package/dist/{inject-session-WQGGMCQY.cjs → inject-session-MLCJMYDG.cjs} +7 -7
  97. package/dist/{inject-session-WQGGMCQY.cjs.map → inject-session-MLCJMYDG.cjs.map} +1 -1
  98. package/dist/{inject-session-BJMJH5BX.js → inject-session-RBQZ45UM.js} +5 -5
  99. package/dist/{inject-session-BJMJH5BX.js.map → inject-session-RBQZ45UM.js.map} +1 -1
  100. package/dist/internal/memory/storage/markdown-store.d.ts +13 -0
  101. package/dist/internal/persistence/index.cjs +17 -16
  102. package/dist/internal/persistence/index.cjs.map +1 -1
  103. package/dist/internal/persistence/index.js +2 -1
  104. package/dist/internal/persistence/index.js.map +1 -1
  105. package/dist/internal/persistence/paths.d.cts +42 -0
  106. package/dist/internal/persistence/paths.d.ts +42 -0
  107. package/dist/internal/persistence/session-transcript.d.cts +40 -1
  108. package/dist/internal/persistence/session-transcript.d.ts +40 -1
  109. package/dist/internal/runtime/plugins/plugin-bundles.d.ts +19 -0
  110. package/dist/persistence.cjs +4 -4
  111. package/dist/persistence.js +1 -1
  112. package/dist/project.cjs +3 -3
  113. package/dist/project.js +1 -1
  114. package/dist/session-transcript-HF5GEU5M.cjs +63 -0
  115. package/dist/{session-transcript-JXHFTB7G.cjs.map → session-transcript-HF5GEU5M.cjs.map} +1 -1
  116. package/dist/session-transcript-P462ZRHX.js +6 -0
  117. package/dist/{session-transcript-AKDYYGXQ.js.map → session-transcript-P462ZRHX.js.map} +1 -1
  118. package/dist/subagents-loader-6ATCDEKN.js +8 -0
  119. package/dist/{subagents-loader-J54ESLDV.js.map → subagents-loader-6ATCDEKN.js.map} +1 -1
  120. package/dist/subagents-loader-X5QKODCL.cjs +17 -0
  121. package/dist/{subagents-loader-AZIXJ7D3.cjs.map → subagents-loader-X5QKODCL.cjs.map} +1 -1
  122. package/dist/subagents-loader.cjs +3 -2
  123. package/dist/subagents-loader.cjs.map +1 -1
  124. package/dist/subagents-loader.js +2 -1
  125. package/dist/subagents-loader.js.map +1 -1
  126. package/docs/error-codes.md +19 -19
  127. package/docs/harness-capability-map.md +1 -1
  128. package/package.json +5 -9
  129. package/dist/agent-session-store-P6V3FINW.js +0 -7
  130. package/dist/agent-session-store-ZJ3JSRS4.cjs +0 -24
  131. package/dist/chunk-2QKTVKH3.js.map +0 -1
  132. package/dist/chunk-53CBTBWO.cjs.map +0 -1
  133. package/dist/chunk-6VLKVX7O.cjs.map +0 -1
  134. package/dist/chunk-DZBSJX6J.js.map +0 -1
  135. package/dist/chunk-GJ6RK75E.js.map +0 -1
  136. package/dist/chunk-GTKFV7O5.js.map +0 -1
  137. package/dist/chunk-HJBMA5MB.cjs.map +0 -1
  138. package/dist/chunk-JHPGF3FP.cjs.map +0 -1
  139. package/dist/chunk-KBXWEWKS.js.map +0 -1
  140. package/dist/chunk-LQYQHXIW.cjs.map +0 -1
  141. package/dist/chunk-ODTZ7HX3.js.map +0 -1
  142. package/dist/chunk-U2AC6JUP.cjs.map +0 -1
  143. package/dist/chunk-UPRJR6IP.js.map +0 -1
  144. package/dist/chunk-YMA4S2WO.js.map +0 -1
  145. package/dist/chunk-YPJ5NH5N.cjs.map +0 -1
  146. package/dist/chunk-ZNW6V4Y6.cjs.map +0 -1
  147. package/dist/context-FDOON2DB.js +0 -6
  148. package/dist/session-transcript-AKDYYGXQ.js +0 -6
  149. package/dist/session-transcript-JXHFTB7G.cjs +0 -51
  150. package/dist/subagents-loader-AZIXJ7D3.cjs +0 -16
  151. package/dist/subagents-loader-J54ESLDV.js +0 -7
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/internal/runtime/context/context-loaders.ts","../src/internal/runtime/context/context-import-resolver.ts","../src/internal/runtime/context/context-glob.ts","../src/internal/runtime/context/context-rules-frontmatter.ts","../src/internal/runtime/context/context-mdc-parser.ts","../src/internal/runtime/context/context-discovery-runner.ts"],"names":["resolvePath","z","dirname","readFile"],"mappings":"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","file":"chunk-7FHZ4VQX.js","sourcesContent":["/**\n * Context file loaders + truncation (T1.2, ADRs D154 / D155 / D159).\n *\n * Reads context source files from disk, applies the per-file size cap\n * with 70%/20% head/tail truncation, and emits the\n * `context_files_truncated` telemetry counter when the cap fires.\n * Pure (no I/O) for `truncateWithMarker` — testable in isolation.\n *\n * @internal\n */\n\nimport { readFile } from \"node:fs/promises\";\n\nconst HEAD_RATIO = 0.7;\nconst TAIL_RATIO = 0.2;\nconst MARKER = \"\\n\\n…[truncated by theokit]\\n\\n\";\n\n/** Default per-file cap (40k chars ≈ 10k tokens). D155. */\nexport const DEFAULT_MAX_BYTES_PER_FILE = 40_000;\n\nexport interface LoadedSource {\n /** Absolute path the content came from. */\n readonly source: string;\n readonly content: string;\n readonly originalBytes: number;\n readonly truncated: boolean;\n}\n\nexport interface TruncateResult {\n readonly truncated: boolean;\n readonly finalContent: string;\n}\n\n/**\n * Cap `content` at `max` characters with 70%/20% head/tail truncation\n * and an explicit marker. Pure — no I/O, no telemetry.\n *\n * EC-C: when `max <= MARKER.length`, skip the marker entirely and\n * return a head-only slice. Without this guard, `budget = max -\n * MARKER.length` would go negative and `content.slice(-tailBytes)`\n * would slice from the END of the string rather than truncating.\n *\n * EC-H: codepoint integrity — `String.slice()` operates on UTF-16\n * code units, which can split surrogate pairs. We accept that\n * trailing/leading replacement chars (U+FFFD) may appear at boundaries.\n * Modern LLMs tolerate them; byte-exact safety would require Buffer\n * slicing + TextDecoder with `fatal: true` retry, which adds complexity\n * without value.\n *\n * @public\n */\nexport function truncateWithMarker(content: string, max: number): TruncateResult {\n if (content.length <= max) {\n return { truncated: false, finalContent: content };\n }\n // EC-C: max too small to fit even the marker — return head-only slice.\n if (max <= MARKER.length) {\n return { truncated: true, finalContent: content.slice(0, max) };\n }\n const budget = max - MARKER.length;\n const headBytes = Math.floor(budget * (HEAD_RATIO / (HEAD_RATIO + TAIL_RATIO)));\n const tailBytes = budget - headBytes;\n return {\n truncated: true,\n finalContent: content.slice(0, headBytes) + MARKER + content.slice(-tailBytes),\n };\n}\n\n/**\n * Read a file from disk and apply the per-file cap. Emits telemetry\n * counter when truncation fires.\n *\n * EC-G: file deleted between discovery and read (FS race) → returns\n * `undefined`, never throws.\n *\n * @internal\n */\nexport async function loadPlainMarkdown(\n absPath: string,\n opts: { maxBytesPerFile?: number } = {},\n): Promise<LoadedSource | undefined> {\n const max = opts.maxBytesPerFile ?? DEFAULT_MAX_BYTES_PER_FILE;\n let content: string;\n try {\n content = await readFile(absPath, \"utf8\");\n } catch {\n // EC-G: ENOENT / EACCES / etc → caller treats as missing.\n return undefined;\n }\n const { truncated, finalContent } = truncateWithMarker(content, max);\n if (truncated) {\n emitTruncationCounter(absPath);\n }\n return {\n source: absPath,\n content: finalContent,\n originalBytes: content.length,\n truncated,\n };\n}\n\n/**\n * EC-L: telemetry no-op when OTel is not imported. We import the tracer\n * lazily and use `safeCall` so the lookup never throws and never pulls\n * OTel into the import graph for users without `@opentelemetry/api`\n * installed.\n *\n * @internal\n */\nfunction emitTruncationCounter(source: string): void {\n // Lazy resolution via globalThis avoids static import of telemetry/tracer.\n // Tests can spy via the same path; production users without OTel see no-op.\n const tracer = (\n globalThis as { __theokit_tracer?: { inc?: (k: string, attrs: unknown) => void } }\n ).__theokit_tracer;\n if (tracer?.inc === undefined) return;\n try {\n tracer.inc(\"context_files_truncated\", { file: source });\n } catch {\n // Telemetry must never break the loader.\n }\n}\n","/**\n * `@path/to/file` import resolver (T2.1, ADR D156).\n *\n * Anthropic/Gemini convention: lines that are EXACTLY `@path` get\n * replaced with the imported file's content, recursively (5-hop cap),\n * with cycle detection. Inline `see @x.md, also @y.md` references are\n * NOT resolved (EC-Q — own-line only, mirrors Anthropic's actual\n * behavior).\n *\n * EC-D fix: every imported file is itself capped at `maxBytesPerFile`\n * via `loadPlainMarkdown` BEFORE concatenation. Prevents a CLAUDE.md\n * with 5 imports of 30k each from ballooning to 150k of imported\n * content before the outer cap fires.\n *\n * @internal\n */\n\nimport { homedir } from \"node:os\";\nimport { dirname, isAbsolute, join, resolve as resolvePath } from \"node:path\";\n\nimport { loadPlainMarkdown } from \"./context-loaders.js\";\nimport { insideRoot } from \"./path-containment.js\";\n\n/** EC-Q: line-anchored. `@path` must be alone on its line. */\nconst IMPORT_RE = /^@(\\S+)\\s*$/gm;\nconst MAX_HOPS = 5;\n\nexport interface ResolveImportsOptions {\n /** Absolute paths already resolved (cycle detection). */\n readonly visited: Set<string>;\n /** Current recursion depth — caps at MAX_HOPS. */\n readonly depth: number;\n /** Per-import file cap (EC-D). Forwarded to loadPlainMarkdown. */\n readonly maxBytesPerFile: number;\n /**\n * The directory an import may not escape. When set, a target resolving outside it is\n * refused and replaced with a placeholder.\n *\n * The file carrying the import is REPOSITORY-CONTROLLED — `CLAUDE.md` and `GEMINI.md`\n * are the two default specs with `followImports: true`, and both are found by\n * `git-root-walk` inside the tree the agent was pointed at. Without a root, a cloned\n * repository could name `@~/.ssh/id_rsa` or any absolute path and have its contents\n * inlined into the system prompt, and from there sent to the model provider. The\n * traversal guard that already existed (`isSafePattern`) guards the discovery PATTERN,\n * not the import TARGET, so it never saw this.\n *\n * OPTIONAL, so a caller outside the discovery path keeps the previous behaviour rather\n * than breaking on an upgrade. `runDiscovery` always supplies `gitRoot ?? cwd` — the\n * same value it already uses to keep absolute paths out of `<source name=\"\">`.\n */\n readonly projectRoot?: string;\n}\n\n/**\n * Resolve `@path` directives in `content`. Each match is replaced with\n * the imported file content (already cap-truncated per EC-D), and\n * recursion continues on the resolved content until MAX_HOPS or cycle.\n *\n * Failure modes (placeholders, never throws):\n * - file not found → `[@import not found: <path>]`\n * - cycle detected → `[@import cycle detected: <path>]`\n * - depth exceeded → trailing `\\n\\n…[@import depth limit 5 reached]\\n\\n`\n *\n * @internal\n */\nexport async function resolveImports(\n content: string,\n basePath: string,\n opts: ResolveImportsOptions,\n): Promise<string> {\n if (opts.depth >= MAX_HOPS) {\n return `${content}\\n\\n…[@import depth limit ${MAX_HOPS} reached]\\n\\n`;\n }\n const baseDir = dirname(basePath);\n return replaceAsync(content, IMPORT_RE, async (raw) => {\n const absolute = resolveImportPath(raw, baseDir);\n // Containment BEFORE the read. Reporting the refusal after loading the file would\n // already have put the bytes in memory, and the placeholder names the path the author\n // wrote rather than the resolved one — echoing `/home/<user>/.ssh/id_rsa` back into the\n // prompt would leak the layout of the machine to the same untrusted document.\n if (opts.projectRoot !== undefined && !insideRoot(absolute, opts.projectRoot)) {\n return `[@import outside the project root, refused: ${raw}]`;\n }\n if (opts.visited.has(absolute)) {\n return `[@import cycle detected: ${raw}]`;\n }\n opts.visited.add(absolute);\n const loaded = await loadPlainMarkdown(absolute, { maxBytesPerFile: opts.maxBytesPerFile });\n if (loaded === undefined) {\n return `[@import not found: ${raw}]`;\n }\n // Recurse with same visited set + incremented depth. `projectRoot` is FORWARDED: an\n // imported file is repository-controlled too, so a root that applied only at depth 0\n // would be escapable in one extra hop.\n return resolveImports(loaded.content, absolute, {\n visited: opts.visited,\n depth: opts.depth + 1,\n maxBytesPerFile: opts.maxBytesPerFile,\n ...(opts.projectRoot === undefined ? {} : { projectRoot: opts.projectRoot }),\n });\n });\n}\n\nfunction resolveImportPath(raw: string, baseDir: string): string {\n if (raw.startsWith(\"~/\")) {\n return resolvePath(join(homedir(), raw.slice(2)));\n }\n if (isAbsolute(raw)) {\n return resolvePath(raw);\n }\n return resolvePath(join(baseDir, raw));\n}\n\n/**\n * Async equivalent of `String.prototype.replace` — sequential. Returns\n * `content` with every `re` match replaced by `await replacer(match)`.\n *\n * @internal\n */\nasync function replaceAsync(\n content: string,\n re: RegExp,\n replacer: (raw: string) => Promise<string>,\n): Promise<string> {\n const matches: Array<{ match: string; raw: string; index: number }> = [];\n // Reset lastIndex; clone the regex to avoid stateful traps.\n const localRe = new RegExp(re.source, re.flags);\n let m: RegExpExecArray | null;\n // biome-ignore lint/suspicious/noAssignInExpressions: idiomatic regex loop\n while ((m = localRe.exec(content)) !== null) {\n matches.push({ match: m[0], raw: m[1] ?? \"\", index: m.index });\n if (m.index === localRe.lastIndex) localRe.lastIndex += 1;\n }\n if (matches.length === 0) return content;\n let result = \"\";\n let cursor = 0;\n for (const entry of matches) {\n result += content.slice(cursor, entry.index);\n result += await replacer(entry.raw);\n cursor = entry.index + entry.match.length;\n }\n result += content.slice(cursor);\n return result;\n}\n","/**\n * Shared glob → RegExp compiler for context discovery (extracted from the\n * MDC parser so the `.cursor/rules/*.mdc` and `.theokit/rules/*.md` parsers\n * share ONE implementation — DRY, no new dependency).\n *\n * Supports `**` (any depth), `*` (single path segment, no `/`), `?` (single\n * non-separator char). Sufficient for the patterns Cursor and Claude Code\n * themselves recommend.\n *\n * @internal\n */\nexport function globToRegex(glob: string): RegExp {\n // Support **/ (zero-or-more path segments), ** (any depth), * (single\n // segment), ? (single non-separator char). `**/` collapses so `src/**/*.ts`\n // matches `src/foo.ts` AND `src/a/b/foo.ts` — the semantics Cursor and Claude\n // Code document (\"src/**/* → all files under src/\"). `*` and `?` never cross a\n // `/` (strict glob segment semantics). Escape other regex metas.\n const compiled = glob\n .replace(/[.+^${}()|[\\]\\\\]/g, \"\\\\$&\")\n .replace(/\\*\\*\\//g, \"::GLOBSTAR_SLASH::\")\n .replace(/\\*\\*/g, \"::GLOBSTAR::\")\n .replace(/\\*/g, \"[^/]*\")\n .replace(/\\?/g, \"[^/]\")\n .replace(/::GLOBSTAR_SLASH::/g, \"(?:.*/)?\")\n .replace(/::GLOBSTAR::/g, \".*\");\n return new RegExp(`^${compiled}$`);\n}\n\n/**\n * True when any of `patterns` glob-matches any of `paths`. Central helper so\n * both rule parsers agree on activation semantics.\n *\n * @internal\n */\nexport function anyGlobMatches(\n patterns: ReadonlyArray<string>,\n paths: ReadonlyArray<string>,\n): boolean {\n if (patterns.length === 0 || paths.length === 0) return false;\n const res = patterns.map(globToRegex);\n return paths.some((p) => res.some((re) => re.test(p)));\n}\n","/**\n * Parser for `.theokit/rules/*.md` — theokit-native path-scoped rules,\n * mirroring Claude Code's `.claude/rules/`.\n *\n * Frontmatter fields:\n * - `paths` — glob-pattern array (Claude Code parity: `.claude/rules` uses `paths:`).\n * - `globs` — glob-pattern array (Cursor-compatible alias; unioned with `paths`).\n * - `alwaysApply` — activate unconditionally (no scope needed).\n * - `enabled` — set `false` to disable the rule entirely (wins over everything).\n * - `description` — human note; not used for activation in v1.\n *\n * Activation (`shouldActivateRule`):\n * - `enabled: false` → never activates.\n * - `alwaysApply: true` → always activates.\n * - otherwise → activates iff a `paths`/`globs` pattern matches an in-scope\n * file (the caller's `agent.send(..., { contextPaths })`). With no in-scope\n * files, a scoped rule stays dormant (parity with Cursor `.mdc`).\n *\n * A file with no `---` frontmatter is treated as an unconditional rule\n * (`alwaysApply: true`). Malformed YAML → `undefined` (caller drops + counts).\n *\n * Glob + YAML subset are shared with the MDC parser (DRY, no new dependency).\n *\n * NOTE — no internal-visibility tag in this block. `tsconfig.base.json` sets `stripInternal: true`,\n * and TypeScript scans EVERY leading comment range of the declaration that follows, including the\n * import right below this one. The tag that used to sit here deleted that import from the emitted\n * `.d.ts`, leaving the types it binds unresolvable for any consumer running type-aware lint\n * (usetheodev/theokit-sdk#283 records the same trap on a declaration).\n */\n\nimport { z } from \"zod\";\n\nimport { anyGlobMatches } from \"./context-glob.js\";\nimport { parseSimpleYaml, splitFrontmatter } from \"./context-yaml-lite.js\";\n\n/**\n * YAML frontmatter schema for `.theokit/rules/*.md` files.\n *\n * Every field is optional, and unknown keys are dropped rather than rejected — `safeParse` failing\n * therefore means a declared key had the wrong TYPE (`paths` as a bare string, `enabled` as the\n * string `\"false\"`), never that an unexpected key was present.\n *\n * Exported because {@link RulesFrontmatter} is inferred from it and appears in the signature of\n * {@link shouldActivateRule}, which `@theokit/sdk/context` publishes: the constant has to reach the\n * emitted declarations for that inferred type to resolve in a consumer's project.\n */\nexport const RulesFrontmatterSchema = z.object({\n description: z.string().optional(),\n paths: z.array(z.string()).optional(),\n globs: z.array(z.string()).optional(),\n alwaysApply: z.boolean().optional(),\n enabled: z.boolean().optional(),\n});\n\nexport type RulesFrontmatter = z.infer<typeof RulesFrontmatterSchema>;\n\nexport interface RulesParseResult {\n readonly frontmatter: RulesFrontmatter;\n readonly body: string;\n}\n\n/**\n * Split a `.theokit/rules/*.md` document into its frontmatter and its body.\n *\n * A file with no `---` fence is not an error: the whole text becomes the body and the frontmatter\n * is synthesised as `{ alwaysApply: true }`, so a plain markdown rule with no metadata applies\n * unconditionally. That default is the one thing to know before adding a fence — adding one and\n * omitting both `alwaysApply` and a path pattern turns an always-on rule into a dormant one.\n *\n * `undefined` means the fence was there and its contents did not survive: YAML the lite parser\n * could not read, or a shape the schema rejected (`paths` as a bare string rather than a list,\n * `enabled` as `\"false\"` rather than `false`). It never throws, and it does not say WHICH of the\n * two happened — the caller drops the file and counts it.\n *\n * Unknown frontmatter keys are dropped rather than rejected, so a misspelled `alwaysAply` parses\n * fine and simply has no effect.\n *\n * Nothing here decides whether the rule applies; that is `shouldActivateRule`.\n *\n * @public — re-exported from `@theokit/sdk/context`, and therefore under semver.\n */\nexport function parseRules(content: string): RulesParseResult | undefined {\n const { yaml, body } = splitFrontmatter(content);\n if (yaml === undefined) {\n return { frontmatter: { alwaysApply: true }, body: content };\n }\n try {\n const parsed = parseSimpleYaml(yaml);\n const validated = RulesFrontmatterSchema.safeParse(parsed);\n if (!validated.success) return undefined;\n return { frontmatter: validated.data, body };\n } catch {\n return undefined;\n }\n}\n\n/**\n * Decide whether a parsed rule applies to this turn, given the files in scope.\n *\n * Checked in strict order, first match wins:\n *\n * 1. `enabled: false` — never applies, whatever else the frontmatter says. This overrides\n * `alwaysApply: true`, which is the point of having it.\n * 2. `alwaysApply: true` — applies, and `inScopePaths` is not consulted.\n * 3. otherwise — applies only if some pattern from `paths` or `globs` matches some path in\n * `inScopePaths`. The two lists are a union, not alternatives; `globs` exists for\n * Cursor compatibility and behaves identically.\n *\n * A scoped rule with an EMPTY `inScopePaths` never applies. That is the case worth planning for:\n * `contextPaths` is empty on a plain `agent.send()`, so a rule scoped by path stays dormant until\n * the caller declares which files the turn is about. It is quiet — a dormant rule looks exactly\n * like a rule that was never written.\n *\n * A rule with a fence but no patterns and no `alwaysApply` also never applies, since step 3 has\n * nothing to match.\n *\n * `description` is not consulted. It is a note for humans, not an activation condition.\n *\n * @public — re-exported from `@theokit/sdk/context`, and therefore under semver.\n */\nexport function shouldActivateRule(\n fm: RulesFrontmatter,\n inScopePaths: ReadonlyArray<string>,\n): boolean {\n if (fm.enabled === false) return false;\n if (fm.alwaysApply === true) return true;\n const patterns = [...(fm.paths ?? []), ...(fm.globs ?? [])];\n if (patterns.length === 0) return false;\n return anyGlobMatches(patterns, inScopePaths);\n}\n","/**\n * MDC (Markdown Cursor) parser for `.cursor/rules/*.mdc` (T3.1, ADR D154).\n *\n * Parses YAML frontmatter with `description` / `globs` / `alwaysApply`\n * fields. Honors activation per Cursor's docs:\n * - `alwaysApply: true` → activates unconditionally.\n * - `alwaysApply: false` + matching glob in `touchedFiles` → activates.\n * - `alwaysApply: false` + no glob match → skipped.\n *\n * **EC-I (v1 semantic):** at `agent.send()` time, `touchedFiles` is empty\n * UNLESS the caller passes `contextPaths` (the in-scope file set). When\n * `contextPaths` is provided, per-glob activation fires; otherwise only\n * `alwaysApply: true` rules activate. Description-based \"agent requested\"\n * classification is out of scope.\n *\n * Glob matching + the YAML subset are shared with the `.theokit/rules/*.md`\n * parser via `context-glob.ts` / `context-yaml-lite.ts` (DRY, no new dep).\n *\n * @internal\n */\n\nimport { z } from \"zod\";\n\nimport { anyGlobMatches } from \"./context-glob.js\";\nimport { parseSimpleYaml, splitFrontmatter } from \"./context-yaml-lite.js\";\n\n/** YAML frontmatter schema for MDC files. @internal */\nexport const McdFrontmatterSchema = z.object({\n description: z.string().optional(),\n globs: z.array(z.string()).optional(),\n alwaysApply: z.boolean().optional(),\n});\n\nexport type McdFrontmatter = z.infer<typeof McdFrontmatterSchema>;\n\nexport interface McdParseResult {\n readonly frontmatter: McdFrontmatter;\n readonly body: string;\n}\n\n/**\n * Parse `.mdc` content. Returns frontmatter + body. EC-18: when the\n * file has no `---` frontmatter, returns `frontmatter: {alwaysApply:\n * true}` so the body is treated as an unconditional rule.\n *\n * On YAML parse error, returns `undefined` and the caller emits a\n * telemetry counter (EC-21).\n *\n * @internal\n */\nexport function parseMdc(content: string): McdParseResult | undefined {\n const { yaml, body } = splitFrontmatter(content);\n if (yaml === undefined) {\n // EC-18: no frontmatter — treat as alwaysApply\n return { frontmatter: { alwaysApply: true }, body: content };\n }\n try {\n const parsed = parseSimpleYaml(yaml);\n const validated = McdFrontmatterSchema.safeParse(parsed);\n if (!validated.success) return undefined;\n return { frontmatter: validated.data, body };\n } catch {\n return undefined;\n }\n}\n\n/**\n * Decide whether an MDC rule activates given the currently-touched\n * files. EC-I: empty `touchedFiles` → only `alwaysApply: true`\n * activates.\n *\n * @internal\n */\nexport function shouldActivate(fm: McdFrontmatter, touchedFiles: ReadonlyArray<string>): boolean {\n if (fm.alwaysApply === true) return true;\n if (fm.globs === undefined || fm.globs.length === 0) return false;\n return anyGlobMatches(fm.globs, touchedFiles);\n}\n","/**\n * Multi-format context discovery runner (T5.1, ADRs D150-D156).\n *\n * Walks `DEFAULT_DISCOVERY_SPECS` (or caller override), loads each\n * spec via the appropriate parser, applies `@import` resolution where\n * declared, and returns a flat list of `AggregatorSource[]` ready for\n * the aggregate cap.\n *\n * **EC-E privacy fix:** source disambiguation uses\n * `relative(gitRoot ?? cwd, ...)` — NEVER absolute paths in\n * `<source name=\"\">`.\n *\n * @internal\n */\n\nimport { readFile } from \"node:fs/promises\";\nimport { dirname, relative } from \"node:path\";\n\nimport type { AggregatorSource } from \"./context-aggregator.js\";\nimport {\n DEFAULT_DISCOVERY_SPECS,\n type DiscoverySpec,\n findGitRoot,\n walkUpForFile,\n walkUpForGlob,\n} from \"./context-discovery.js\";\nimport { resolveImports } from \"./context-import-resolver.js\";\nimport { loadPlainMarkdown } from \"./context-loaders.js\";\nimport { parseMdc, shouldActivate } from \"./context-mdc-parser.js\";\nimport { parseRules, shouldActivateRule } from \"./context-rules-frontmatter.js\";\n\n/**\n * Input to the context-discovery run: where to walk, how much of each file to keep, and which\n * trust boundary an `@import` may not cross.\n *\n * `cwd` and `maxBytesPerFile` are required because neither has a safe default — an unset root walks\n * the wrong tree, and an unset cap lets one large file consume the context window.\n *\n * `importRoot` is the field to reach for when the caller's trust boundary is narrower than the\n * repository. Left unset, the repository IS the boundary (`gitRoot ?? cwd`), which is the honest\n * default for a document found by walking the repository — but it does mean an `@import` can pull\n * in any file the repo contains.\n *\n * @public — re-exported from '@theokit/sdk/context', and therefore under semver.\n */\nexport interface DiscoveryRunnerOptions {\n /** Workspace root passed to all discovery scopes. */\n readonly cwd: string;\n /** Per-file truncation cap (D155). */\n readonly maxBytesPerFile: number;\n /** Optional override of the default registry. */\n readonly specs?: ReadonlyArray<DiscoverySpec>;\n /** Cursor MDC: file paths the LLM has touched this turn (EC-I: empty at send-time). */\n readonly touchedFiles?: ReadonlyArray<string>;\n /** When true, skip `theokit-context` spec — caller already handles the legacy path. */\n readonly skipLegacyTheokitContext?: boolean;\n /**\n * The root an `@import` may not escape. Defaults to `gitRoot ?? cwd` — the same value\n * this runner already uses to keep absolute paths out of `<source name=\"\">`.\n *\n * Present so an embedder with a trust boundary narrower than the repository can declare\n * it. Absent, the repository IS the boundary, which is the honest default for a document\n * discovered by walking the repository.\n */\n readonly importRoot?: string;\n}\n\n/**\n * Find, read and parse every context file the specs describe, and return them ready for the\n * aggregator.\n *\n * Specs are processed in the order given — `opts.specs` when supplied, otherwise\n * `DEFAULT_DISCOVERY_SPECS` — and within a spec, in the order its scope resolves paths. The\n * returned array carries `priority` on each source; it is NOT sorted here, so the aggregator is\n * what applies the ordering.\n *\n * A path already emitted is skipped, across specs as well as within one. Paths arrive resolved\n * through `realpath`, so two specs pointing at the same physical file through a symlink produce\n * one source rather than two.\n *\n * Missing and unreadable files are not errors: a file that does not exist is simply not matched,\n * and one that fails to read or parse is dropped and the run continues. A frontmatter file whose\n * activation conditions do not hold — `enabled: false`, or a scope that no touched file matches —\n * is dropped the same way. So a shorter result than expected means \"nothing qualified\", and this\n * function will not tell you which of those it was.\n *\n * Reads the filesystem and nothing else. No network, no writes, and no mutation of `opts`.\n *\n * Privacy: `<source name=\"\">` is built from the path RELATIVE to the git root (or `cwd` when\n * there is no git root), never an absolute path, so a home directory never reaches the prompt.\n * `@import` resolution is bounded by `opts.importRoot`, defaulting to that same root — a document\n * cannot pull in a file from outside the repository.\n *\n * @public — re-exported from `@theokit/sdk/context`, and therefore under semver.\n */\n// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: per-spec dispatch ladder + cross-spec dedup is a flat orchestrator; splitting would obscure the priority-merge invariant.\nexport async function runDiscovery(opts: DiscoveryRunnerOptions): Promise<AggregatorSource[]> {\n const gitRoot = findGitRoot(opts.cwd);\n const specs = opts.specs ?? DEFAULT_DISCOVERY_SPECS;\n const out: AggregatorSource[] = [];\n const seenReal = new Set<string>();\n\n for (const spec of specs) {\n if (opts.skipLegacyTheokitContext && spec.id === \"theokit-context\") continue;\n const paths = await resolvePathsForSpec(spec, opts.cwd, gitRoot);\n for (const path of paths) {\n if (seenReal.has(path)) continue;\n seenReal.add(path);\n const source = await loadOneSource(spec, path, opts, gitRoot);\n if (source !== undefined) out.push(source);\n }\n }\n return out;\n}\n\nasync function resolvePathsForSpec(\n spec: DiscoverySpec,\n cwd: string,\n gitRoot: string | undefined,\n): Promise<string[]> {\n if (spec.scope === \"cwd-only\") {\n return walkUpForFile(cwd, spec.pattern, cwd);\n }\n if (spec.scope === \"git-root-walk\") {\n return walkUpForFile(cwd, spec.pattern, gitRoot ?? cwd);\n }\n // globbed\n return walkUpForGlob(cwd, spec.pattern);\n}\n\nasync function loadOneSource(\n spec: DiscoverySpec,\n path: string,\n opts: DiscoveryRunnerOptions,\n gitRoot: string | undefined,\n): Promise<AggregatorSource | undefined> {\n // EC-E privacy: name uses relative-to-git-root for disambiguation;\n // NEVER absolute paths in the public `<source name=\"\">` attribute.\n // For cwd-only specs (THEO.md), no disambiguation is needed because\n // the scope can only ever match a single file.\n const relPath = relative(gitRoot ?? opts.cwd, dirname(path));\n const needsSuffix = spec.scope !== \"cwd-only\" && relPath !== \"\" && relPath !== \".\";\n const id = needsSuffix ? `${spec.id}@${relPath}` : spec.id;\n\n if (spec.parser === \"mdc\") {\n return loadMdcSource(spec, path, id, opts);\n }\n if (spec.parser === \"rules-frontmatter\") {\n return loadRulesSource(spec, path, id, opts);\n }\n if (spec.parser === \"frontmatter-zod\") {\n // Legacy `.theokit/context/*.md` — handled by `loadContextConfig` in\n // `context-manager.ts` for backward compat. We skip here unless caller\n // explicitly wants us to load it (currently always skipped).\n return undefined;\n }\n // plain-markdown\n return loadPlainMarkdownSource(spec, path, id, opts, gitRoot);\n}\n\n/**\n * Shared read → parse → activation-gate → source pipeline for the frontmatter\n * discovery parsers (mdc, rules). Binds the parser + the activation predicate so\n * the mdc and rules loaders differ only in those two, not in the surrounding\n * read/guard/shape boilerplate (DRY).\n */\nasync function loadParsedSource<F>(\n spec: DiscoverySpec,\n path: string,\n id: string,\n parse: (raw: string) => { frontmatter: F; body: string } | undefined,\n isActive: (frontmatter: F) => boolean,\n): Promise<AggregatorSource | undefined> {\n let raw: string;\n try {\n raw = await readFile(path, \"utf8\");\n } catch {\n return undefined;\n }\n const parsed = parse(raw);\n if (parsed === undefined || !isActive(parsed.frontmatter)) return undefined;\n return { id, source: path, content: parsed.body, priority: spec.priority, truncated: false };\n}\n\nfunction loadMdcSource(\n spec: DiscoverySpec,\n path: string,\n id: string,\n opts: DiscoveryRunnerOptions,\n): Promise<AggregatorSource | undefined> {\n return loadParsedSource(spec, path, id, parseMdc, (fm) =>\n shouldActivate(fm, opts.touchedFiles ?? []),\n );\n}\n\nfunction loadRulesSource(\n spec: DiscoverySpec,\n path: string,\n id: string,\n opts: DiscoveryRunnerOptions,\n): Promise<AggregatorSource | undefined> {\n return loadParsedSource(spec, path, id, parseRules, (fm) =>\n shouldActivateRule(fm, opts.touchedFiles ?? []),\n );\n}\n\nasync function loadPlainMarkdownSource(\n spec: DiscoverySpec,\n path: string,\n id: string,\n opts: DiscoveryRunnerOptions,\n gitRoot: string | undefined,\n): Promise<AggregatorSource | undefined> {\n const loaded = await loadPlainMarkdown(path, { maxBytesPerFile: opts.maxBytesPerFile });\n if (loaded === undefined) return undefined;\n let content = loaded.content;\n if (spec.followImports) {\n content = await resolveImports(content, path, {\n visited: new Set([path]),\n depth: 0,\n maxBytesPerFile: opts.maxBytesPerFile,\n // The document carrying the import is repository-controlled, so the repository is the\n // boundary it may not cross. Without this, `CLAUDE.md` / `GEMINI.md` — the two specs\n // with `followImports: true` — could name any absolute or `~/` path and have it\n // inlined into the system prompt.\n projectRoot: opts.importRoot ?? gitRoot ?? opts.cwd,\n });\n }\n return {\n id,\n source: path,\n content,\n priority: spec.priority,\n truncated: loaded.truncated,\n };\n}\n"]}
1
+ {"version":3,"sources":["../src/internal/runtime/context/context-loaders.ts","../src/internal/runtime/context/context-import-resolver.ts","../src/internal/runtime/context/context-glob.ts","../src/internal/runtime/context/context-rules-frontmatter.ts","../src/internal/runtime/context/context-mdc-parser.ts","../src/internal/runtime/context/context-discovery-runner.ts"],"names":["resolvePath","z","dirname","readFile"],"mappings":"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","file":"chunk-SAVIWMZB.js","sourcesContent":["/**\n * Context file loaders + truncation (T1.2, ADRs D154 / D155 / D159).\n *\n * Reads context source files from disk, applies the per-file size cap\n * with 70%/20% head/tail truncation, and emits the\n * `context_files_truncated` telemetry counter when the cap fires.\n * Pure (no I/O) for `truncateWithMarker` — testable in isolation.\n *\n * @internal\n */\n\nimport { readFile } from \"node:fs/promises\";\n\nconst HEAD_RATIO = 0.7;\nconst TAIL_RATIO = 0.2;\nconst MARKER = \"\\n\\n…[truncated by theokit]\\n\\n\";\n\n/** Default per-file cap (40k chars ≈ 10k tokens). D155. */\nexport const DEFAULT_MAX_BYTES_PER_FILE = 40_000;\n\nexport interface LoadedSource {\n /** Absolute path the content came from. */\n readonly source: string;\n readonly content: string;\n readonly originalBytes: number;\n readonly truncated: boolean;\n}\n\nexport interface TruncateResult {\n readonly truncated: boolean;\n readonly finalContent: string;\n}\n\n/**\n * Cap `content` at `max` characters with 70%/20% head/tail truncation\n * and an explicit marker. Pure — no I/O, no telemetry.\n *\n * EC-C: when `max <= MARKER.length`, skip the marker entirely and\n * return a head-only slice. Without this guard, `budget = max -\n * MARKER.length` would go negative and `content.slice(-tailBytes)`\n * would slice from the END of the string rather than truncating.\n *\n * EC-H: codepoint integrity — `String.slice()` operates on UTF-16\n * code units, which can split surrogate pairs. We accept that\n * trailing/leading replacement chars (U+FFFD) may appear at boundaries.\n * Modern LLMs tolerate them; byte-exact safety would require Buffer\n * slicing + TextDecoder with `fatal: true` retry, which adds complexity\n * without value.\n *\n * @public\n */\nexport function truncateWithMarker(content: string, max: number): TruncateResult {\n if (content.length <= max) {\n return { truncated: false, finalContent: content };\n }\n // EC-C: max too small to fit even the marker — return head-only slice.\n if (max <= MARKER.length) {\n return { truncated: true, finalContent: content.slice(0, max) };\n }\n const budget = max - MARKER.length;\n const headBytes = Math.floor(budget * (HEAD_RATIO / (HEAD_RATIO + TAIL_RATIO)));\n const tailBytes = budget - headBytes;\n return {\n truncated: true,\n finalContent: content.slice(0, headBytes) + MARKER + content.slice(-tailBytes),\n };\n}\n\n/**\n * Read a file from disk and apply the per-file cap. Emits telemetry\n * counter when truncation fires.\n *\n * EC-G: file deleted between discovery and read (FS race) → returns\n * `undefined`, never throws.\n *\n * @internal\n */\nexport async function loadPlainMarkdown(\n absPath: string,\n opts: { maxBytesPerFile?: number } = {},\n): Promise<LoadedSource | undefined> {\n const max = opts.maxBytesPerFile ?? DEFAULT_MAX_BYTES_PER_FILE;\n let content: string;\n try {\n content = await readFile(absPath, \"utf8\");\n } catch {\n // EC-G: ENOENT / EACCES / etc → caller treats as missing.\n return undefined;\n }\n const { truncated, finalContent } = truncateWithMarker(content, max);\n if (truncated) {\n emitTruncationCounter(absPath);\n }\n return {\n source: absPath,\n content: finalContent,\n originalBytes: content.length,\n truncated,\n };\n}\n\n/**\n * EC-L: telemetry no-op when OTel is not imported. We import the tracer\n * lazily and use `safeCall` so the lookup never throws and never pulls\n * OTel into the import graph for users without `@opentelemetry/api`\n * installed.\n *\n * @internal\n */\nfunction emitTruncationCounter(source: string): void {\n // Lazy resolution via globalThis avoids static import of telemetry/tracer.\n // Tests can spy via the same path; production users without OTel see no-op.\n const tracer = (\n globalThis as { __theokit_tracer?: { inc?: (k: string, attrs: unknown) => void } }\n ).__theokit_tracer;\n if (tracer?.inc === undefined) return;\n try {\n tracer.inc(\"context_files_truncated\", { file: source });\n } catch {\n // Telemetry must never break the loader.\n }\n}\n","/**\n * `@path/to/file` import resolver (T2.1, ADR D156).\n *\n * Anthropic/Gemini convention: lines that are EXACTLY `@path` get\n * replaced with the imported file's content, recursively (5-hop cap),\n * with cycle detection. Inline `see @x.md, also @y.md` references are\n * NOT resolved (EC-Q — own-line only, mirrors Anthropic's actual\n * behavior).\n *\n * EC-D fix: every imported file is itself capped at `maxBytesPerFile`\n * via `loadPlainMarkdown` BEFORE concatenation. Prevents a CLAUDE.md\n * with 5 imports of 30k each from ballooning to 150k of imported\n * content before the outer cap fires.\n *\n * @internal\n */\n\nimport { homedir } from \"node:os\";\nimport { dirname, isAbsolute, join, resolve as resolvePath } from \"node:path\";\n\nimport { loadPlainMarkdown } from \"./context-loaders.js\";\nimport { insideRoot } from \"./path-containment.js\";\n\n/** EC-Q: line-anchored. `@path` must be alone on its line. */\nconst IMPORT_RE = /^@(\\S+)\\s*$/gm;\nconst MAX_HOPS = 5;\n\nexport interface ResolveImportsOptions {\n /** Absolute paths already resolved (cycle detection). */\n readonly visited: Set<string>;\n /** Current recursion depth — caps at MAX_HOPS. */\n readonly depth: number;\n /** Per-import file cap (EC-D). Forwarded to loadPlainMarkdown. */\n readonly maxBytesPerFile: number;\n /**\n * The directory an import may not escape. When set, a target resolving outside it is\n * refused and replaced with a placeholder.\n *\n * The file carrying the import is REPOSITORY-CONTROLLED — `CLAUDE.md` and `GEMINI.md`\n * are the two default specs with `followImports: true`, and both are found by\n * `git-root-walk` inside the tree the agent was pointed at. Without a root, a cloned\n * repository could name `@~/.ssh/id_rsa` or any absolute path and have its contents\n * inlined into the system prompt, and from there sent to the model provider. The\n * traversal guard that already existed (`isSafePattern`) guards the discovery PATTERN,\n * not the import TARGET, so it never saw this.\n *\n * OPTIONAL, so a caller outside the discovery path keeps the previous behaviour rather\n * than breaking on an upgrade. `runDiscovery` always supplies `gitRoot ?? cwd` — the\n * same value it already uses to keep absolute paths out of `<source name=\"\">`.\n */\n readonly projectRoot?: string;\n}\n\n/**\n * Resolve `@path` directives in `content`. Each match is replaced with\n * the imported file content (already cap-truncated per EC-D), and\n * recursion continues on the resolved content until MAX_HOPS or cycle.\n *\n * Failure modes (placeholders, never throws):\n * - file not found → `[@import not found: <path>]`\n * - cycle detected → `[@import cycle detected: <path>]`\n * - depth exceeded → trailing `\\n\\n…[@import depth limit 5 reached]\\n\\n`\n *\n * @internal\n */\nexport async function resolveImports(\n content: string,\n basePath: string,\n opts: ResolveImportsOptions,\n): Promise<string> {\n if (opts.depth >= MAX_HOPS) {\n return `${content}\\n\\n…[@import depth limit ${MAX_HOPS} reached]\\n\\n`;\n }\n const baseDir = dirname(basePath);\n return replaceAsync(content, IMPORT_RE, async (raw) => {\n const absolute = resolveImportPath(raw, baseDir);\n // Containment BEFORE the read. Reporting the refusal after loading the file would\n // already have put the bytes in memory, and the placeholder names the path the author\n // wrote rather than the resolved one — echoing `/home/<user>/.ssh/id_rsa` back into the\n // prompt would leak the layout of the machine to the same untrusted document.\n if (opts.projectRoot !== undefined && !insideRoot(absolute, opts.projectRoot)) {\n return `[@import outside the project root, refused: ${raw}]`;\n }\n if (opts.visited.has(absolute)) {\n return `[@import cycle detected: ${raw}]`;\n }\n opts.visited.add(absolute);\n const loaded = await loadPlainMarkdown(absolute, { maxBytesPerFile: opts.maxBytesPerFile });\n if (loaded === undefined) {\n return `[@import not found: ${raw}]`;\n }\n // Recurse with same visited set + incremented depth. `projectRoot` is FORWARDED: an\n // imported file is repository-controlled too, so a root that applied only at depth 0\n // would be escapable in one extra hop.\n return resolveImports(loaded.content, absolute, {\n visited: opts.visited,\n depth: opts.depth + 1,\n maxBytesPerFile: opts.maxBytesPerFile,\n ...(opts.projectRoot === undefined ? {} : { projectRoot: opts.projectRoot }),\n });\n });\n}\n\nfunction resolveImportPath(raw: string, baseDir: string): string {\n if (raw.startsWith(\"~/\")) {\n return resolvePath(join(homedir(), raw.slice(2)));\n }\n if (isAbsolute(raw)) {\n return resolvePath(raw);\n }\n return resolvePath(join(baseDir, raw));\n}\n\n/**\n * Async equivalent of `String.prototype.replace` — sequential. Returns\n * `content` with every `re` match replaced by `await replacer(match)`.\n *\n * @internal\n */\nasync function replaceAsync(\n content: string,\n re: RegExp,\n replacer: (raw: string) => Promise<string>,\n): Promise<string> {\n const matches: Array<{ match: string; raw: string; index: number }> = [];\n // Reset lastIndex; clone the regex to avoid stateful traps.\n const localRe = new RegExp(re.source, re.flags);\n let m: RegExpExecArray | null;\n // biome-ignore lint/suspicious/noAssignInExpressions: idiomatic regex loop\n while ((m = localRe.exec(content)) !== null) {\n matches.push({ match: m[0], raw: m[1] ?? \"\", index: m.index });\n if (m.index === localRe.lastIndex) localRe.lastIndex += 1;\n }\n if (matches.length === 0) return content;\n let result = \"\";\n let cursor = 0;\n for (const entry of matches) {\n result += content.slice(cursor, entry.index);\n result += await replacer(entry.raw);\n cursor = entry.index + entry.match.length;\n }\n result += content.slice(cursor);\n return result;\n}\n","/**\n * Shared glob → RegExp compiler for context discovery (extracted from the\n * MDC parser so the `.cursor/rules/*.mdc` and `.theokit/rules/*.md` parsers\n * share ONE implementation — DRY, no new dependency).\n *\n * Supports `**` (any depth), `*` (single path segment, no `/`), `?` (single\n * non-separator char). Sufficient for the patterns Cursor and Claude Code\n * themselves recommend.\n *\n * @internal\n */\nexport function globToRegex(glob: string): RegExp {\n // Support **/ (zero-or-more path segments), ** (any depth), * (single\n // segment), ? (single non-separator char). `**/` collapses so `src/**/*.ts`\n // matches `src/foo.ts` AND `src/a/b/foo.ts` — the semantics Cursor and Claude\n // Code document (\"src/**/* → all files under src/\"). `*` and `?` never cross a\n // `/` (strict glob segment semantics). Escape other regex metas.\n const compiled = glob\n .replace(/[.+^${}()|[\\]\\\\]/g, \"\\\\$&\")\n .replace(/\\*\\*\\//g, \"::GLOBSTAR_SLASH::\")\n .replace(/\\*\\*/g, \"::GLOBSTAR::\")\n .replace(/\\*/g, \"[^/]*\")\n .replace(/\\?/g, \"[^/]\")\n .replace(/::GLOBSTAR_SLASH::/g, \"(?:.*/)?\")\n .replace(/::GLOBSTAR::/g, \".*\");\n return new RegExp(`^${compiled}$`);\n}\n\n/**\n * True when any of `patterns` glob-matches any of `paths`. Central helper so\n * both rule parsers agree on activation semantics.\n *\n * @internal\n */\nexport function anyGlobMatches(\n patterns: ReadonlyArray<string>,\n paths: ReadonlyArray<string>,\n): boolean {\n if (patterns.length === 0 || paths.length === 0) return false;\n const res = patterns.map(globToRegex);\n return paths.some((p) => res.some((re) => re.test(p)));\n}\n","/**\n * Parser for `.theokit/rules/*.md` — theokit-native path-scoped rules,\n * mirroring Claude Code's `.claude/rules/`.\n *\n * Frontmatter fields:\n * - `paths` — glob-pattern array (Claude Code parity: `.claude/rules` uses `paths:`).\n * - `globs` — glob-pattern array (Cursor-compatible alias; unioned with `paths`).\n * - `alwaysApply` — activate unconditionally (no scope needed).\n * - `enabled` — set `false` to disable the rule entirely (wins over everything).\n * - `description` — human note; not used for activation in v1.\n *\n * Activation (`shouldActivateRule`):\n * - `enabled: false` → never activates.\n * - `alwaysApply: true` → always activates.\n * - otherwise → activates iff a `paths`/`globs` pattern matches an in-scope\n * file (the caller's `agent.send(..., { contextPaths })`). With no in-scope\n * files, a scoped rule stays dormant (parity with Cursor `.mdc`).\n *\n * A file with no `---` frontmatter is treated as an unconditional rule\n * (`alwaysApply: true`). Malformed YAML → `undefined` (caller drops + counts).\n *\n * Glob + YAML subset are shared with the MDC parser (DRY, no new dependency).\n *\n * NOTE — no internal-visibility tag in this block. `tsconfig.base.json` sets `stripInternal: true`,\n * and TypeScript scans EVERY leading comment range of the declaration that follows, including the\n * import right below this one. The tag that used to sit here deleted that import from the emitted\n * `.d.ts`, leaving the types it binds unresolvable for any consumer running type-aware lint\n * (usetheodev/theokit-sdk#283 records the same trap on a declaration).\n */\n\nimport { z } from \"zod\";\n\nimport { anyGlobMatches } from \"./context-glob.js\";\nimport { parseSimpleYaml, splitFrontmatter } from \"./context-yaml-lite.js\";\n\n/**\n * YAML frontmatter schema for `.theokit/rules/*.md` files.\n *\n * Every field is optional, and unknown keys are dropped rather than rejected — `safeParse` failing\n * therefore means a declared key had the wrong TYPE (`paths` as a bare string, `enabled` as the\n * string `\"false\"`), never that an unexpected key was present.\n *\n * Exported because {@link RulesFrontmatter} is inferred from it and appears in the signature of\n * {@link shouldActivateRule}, which `@theokit/sdk/context` publishes: the constant has to reach the\n * emitted declarations for that inferred type to resolve in a consumer's project.\n */\nexport const RulesFrontmatterSchema = z.object({\n description: z.string().optional(),\n paths: z.array(z.string()).optional(),\n globs: z.array(z.string()).optional(),\n alwaysApply: z.boolean().optional(),\n enabled: z.boolean().optional(),\n});\n\nexport type RulesFrontmatter = z.infer<typeof RulesFrontmatterSchema>;\n\nexport interface RulesParseResult {\n readonly frontmatter: RulesFrontmatter;\n readonly body: string;\n}\n\n/**\n * Split a `.theokit/rules/*.md` document into its frontmatter and its body.\n *\n * A file with no `---` fence is not an error: the whole text becomes the body and the frontmatter\n * is synthesised as `{ alwaysApply: true }`, so a plain markdown rule with no metadata applies\n * unconditionally. That default is the one thing to know before adding a fence — adding one and\n * omitting both `alwaysApply` and a path pattern turns an always-on rule into a dormant one.\n *\n * `undefined` means the fence was there and its contents did not survive: YAML the lite parser\n * could not read, or a shape the schema rejected (`paths` as a bare string rather than a list,\n * `enabled` as `\"false\"` rather than `false`). It never throws, and it does not say WHICH of the\n * two happened — the caller drops the file and counts it.\n *\n * Unknown frontmatter keys are dropped rather than rejected, so a misspelled `alwaysAply` parses\n * fine and simply has no effect.\n *\n * Nothing here decides whether the rule applies; that is `shouldActivateRule`.\n *\n * @public — re-exported from `@theokit/sdk/context`, and therefore under semver.\n */\nexport function parseRules(content: string): RulesParseResult | undefined {\n const { yaml, body } = splitFrontmatter(content);\n if (yaml === undefined) {\n return { frontmatter: { alwaysApply: true }, body: content };\n }\n try {\n const parsed = parseSimpleYaml(yaml);\n const validated = RulesFrontmatterSchema.safeParse(parsed);\n if (!validated.success) return undefined;\n return { frontmatter: validated.data, body };\n } catch {\n return undefined;\n }\n}\n\n/**\n * Decide whether a parsed rule applies to this turn, given the files in scope.\n *\n * Checked in strict order, first match wins:\n *\n * 1. `enabled: false` — never applies, whatever else the frontmatter says. This overrides\n * `alwaysApply: true`, which is the point of having it.\n * 2. `alwaysApply: true` — applies, and `inScopePaths` is not consulted.\n * 3. otherwise — applies only if some pattern from `paths` or `globs` matches some path in\n * `inScopePaths`. The two lists are a union, not alternatives; `globs` exists for\n * Cursor compatibility and behaves identically.\n *\n * A scoped rule with an EMPTY `inScopePaths` never applies. That is the case worth planning for:\n * `contextPaths` is empty on a plain `agent.send()`, so a rule scoped by path stays dormant until\n * the caller declares which files the turn is about. It is quiet — a dormant rule looks exactly\n * like a rule that was never written.\n *\n * A rule with a fence but no patterns and no `alwaysApply` also never applies, since step 3 has\n * nothing to match.\n *\n * `description` is not consulted. It is a note for humans, not an activation condition.\n *\n * @public — re-exported from `@theokit/sdk/context`, and therefore under semver.\n */\nexport function shouldActivateRule(\n fm: RulesFrontmatter,\n inScopePaths: ReadonlyArray<string>,\n): boolean {\n if (fm.enabled === false) return false;\n if (fm.alwaysApply === true) return true;\n const patterns = [...(fm.paths ?? []), ...(fm.globs ?? [])];\n if (patterns.length === 0) return false;\n return anyGlobMatches(patterns, inScopePaths);\n}\n","/**\n * MDC (Markdown Cursor) parser for `.cursor/rules/*.mdc` (T3.1, ADR D154).\n *\n * Parses YAML frontmatter with `description` / `globs` / `alwaysApply`\n * fields. Honors activation per Cursor's docs:\n * - `alwaysApply: true` → activates unconditionally.\n * - `alwaysApply: false` + matching glob in `touchedFiles` → activates.\n * - `alwaysApply: false` + no glob match → skipped.\n *\n * **EC-I (v1 semantic):** at `agent.send()` time, `touchedFiles` is empty\n * UNLESS the caller passes `contextPaths` (the in-scope file set). When\n * `contextPaths` is provided, per-glob activation fires; otherwise only\n * `alwaysApply: true` rules activate. Description-based \"agent requested\"\n * classification is out of scope.\n *\n * Glob matching + the YAML subset are shared with the `.theokit/rules/*.md`\n * parser via `context-glob.ts` / `context-yaml-lite.ts` (DRY, no new dep).\n *\n * @internal\n */\n\nimport { z } from \"zod\";\n\nimport { anyGlobMatches } from \"./context-glob.js\";\nimport { parseSimpleYaml, splitFrontmatter } from \"./context-yaml-lite.js\";\n\n/** YAML frontmatter schema for MDC files. @internal */\nexport const McdFrontmatterSchema = z.object({\n description: z.string().optional(),\n globs: z.array(z.string()).optional(),\n alwaysApply: z.boolean().optional(),\n});\n\nexport type McdFrontmatter = z.infer<typeof McdFrontmatterSchema>;\n\nexport interface McdParseResult {\n readonly frontmatter: McdFrontmatter;\n readonly body: string;\n}\n\n/**\n * Parse `.mdc` content. Returns frontmatter + body. EC-18: when the\n * file has no `---` frontmatter, returns `frontmatter: {alwaysApply:\n * true}` so the body is treated as an unconditional rule.\n *\n * On YAML parse error, returns `undefined` and the caller emits a\n * telemetry counter (EC-21).\n *\n * @internal\n */\nexport function parseMdc(content: string): McdParseResult | undefined {\n const { yaml, body } = splitFrontmatter(content);\n if (yaml === undefined) {\n // EC-18: no frontmatter — treat as alwaysApply\n return { frontmatter: { alwaysApply: true }, body: content };\n }\n try {\n const parsed = parseSimpleYaml(yaml);\n const validated = McdFrontmatterSchema.safeParse(parsed);\n if (!validated.success) return undefined;\n return { frontmatter: validated.data, body };\n } catch {\n return undefined;\n }\n}\n\n/**\n * Decide whether an MDC rule activates given the currently-touched\n * files. EC-I: empty `touchedFiles` → only `alwaysApply: true`\n * activates.\n *\n * @internal\n */\nexport function shouldActivate(fm: McdFrontmatter, touchedFiles: ReadonlyArray<string>): boolean {\n if (fm.alwaysApply === true) return true;\n if (fm.globs === undefined || fm.globs.length === 0) return false;\n return anyGlobMatches(fm.globs, touchedFiles);\n}\n","/**\n * Multi-format context discovery runner (T5.1, ADRs D150-D156).\n *\n * Walks `DEFAULT_DISCOVERY_SPECS` (or caller override), loads each\n * spec via the appropriate parser, applies `@import` resolution where\n * declared, and returns a flat list of `AggregatorSource[]` ready for\n * the aggregate cap.\n *\n * **EC-E privacy fix:** source disambiguation uses\n * `relative(gitRoot ?? cwd, ...)` — NEVER absolute paths in\n * `<source name=\"\">`.\n *\n * @internal\n */\n\nimport { readFile } from \"node:fs/promises\";\nimport { dirname, relative } from \"node:path\";\n\nimport type { AggregatorSource } from \"./context-aggregator.js\";\nimport {\n DEFAULT_DISCOVERY_SPECS,\n type DiscoverySpec,\n findGitRoot,\n walkUpForFile,\n walkUpForGlob,\n} from \"./context-discovery.js\";\nimport { resolveImports } from \"./context-import-resolver.js\";\nimport { loadPlainMarkdown } from \"./context-loaders.js\";\nimport { parseMdc, shouldActivate } from \"./context-mdc-parser.js\";\nimport { parseRules, shouldActivateRule } from \"./context-rules-frontmatter.js\";\n\n/**\n * Input to the context-discovery run: where to walk, how much of each file to keep, and which\n * trust boundary an `@import` may not cross.\n *\n * `cwd` and `maxBytesPerFile` are required because neither has a safe default — an unset root walks\n * the wrong tree, and an unset cap lets one large file consume the context window.\n *\n * `importRoot` is the field to reach for when the caller's trust boundary is narrower than the\n * repository. Left unset, the repository IS the boundary (`gitRoot ?? cwd`), which is the honest\n * default for a document found by walking the repository — but it does mean an `@import` can pull\n * in any file the repo contains.\n *\n * @public — re-exported from '@theokit/sdk/context', and therefore under semver.\n */\nexport interface DiscoveryRunnerOptions {\n /** Workspace root passed to all discovery scopes. */\n readonly cwd: string;\n /** Per-file truncation cap (D155). */\n readonly maxBytesPerFile: number;\n /** Optional override of the default registry. */\n readonly specs?: ReadonlyArray<DiscoverySpec>;\n /** Cursor MDC: file paths the LLM has touched this turn (EC-I: empty at send-time). */\n readonly touchedFiles?: ReadonlyArray<string>;\n /** When true, skip `theokit-context` spec — caller already handles the legacy path. */\n readonly skipLegacyTheokitContext?: boolean;\n /**\n * The root an `@import` may not escape. Defaults to `gitRoot ?? cwd` — the same value\n * this runner already uses to keep absolute paths out of `<source name=\"\">`.\n *\n * Present so an embedder with a trust boundary narrower than the repository can declare\n * it. Absent, the repository IS the boundary, which is the honest default for a document\n * discovered by walking the repository.\n */\n readonly importRoot?: string;\n}\n\n/**\n * Find, read and parse every context file the specs describe, and return them ready for the\n * aggregator.\n *\n * Specs are processed in the order given — `opts.specs` when supplied, otherwise\n * `DEFAULT_DISCOVERY_SPECS` — and within a spec, in the order its scope resolves paths. The\n * returned array carries `priority` on each source; it is NOT sorted here, so the aggregator is\n * what applies the ordering.\n *\n * A path already emitted is skipped, across specs as well as within one. Paths arrive resolved\n * through `realpath`, so two specs pointing at the same physical file through a symlink produce\n * one source rather than two.\n *\n * Missing and unreadable files are not errors: a file that does not exist is simply not matched,\n * and one that fails to read or parse is dropped and the run continues. A frontmatter file whose\n * activation conditions do not hold — `enabled: false`, or a scope that no touched file matches —\n * is dropped the same way. So a shorter result than expected means \"nothing qualified\", and this\n * function will not tell you which of those it was.\n *\n * Reads the filesystem and nothing else. No network, no writes, and no mutation of `opts`.\n *\n * Privacy: `<source name=\"\">` is built from the path RELATIVE to the git root (or `cwd` when\n * there is no git root), never an absolute path, so a home directory never reaches the prompt.\n * `@import` resolution is bounded by `opts.importRoot`, defaulting to that same root — a document\n * cannot pull in a file from outside the repository.\n *\n * @public — re-exported from `@theokit/sdk/context`, and therefore under semver.\n */\n// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: per-spec dispatch ladder + cross-spec dedup is a flat orchestrator; splitting would obscure the priority-merge invariant.\nexport async function runDiscovery(opts: DiscoveryRunnerOptions): Promise<AggregatorSource[]> {\n const gitRoot = findGitRoot(opts.cwd);\n const specs = opts.specs ?? DEFAULT_DISCOVERY_SPECS;\n const out: AggregatorSource[] = [];\n const seenReal = new Set<string>();\n\n for (const spec of specs) {\n if (opts.skipLegacyTheokitContext && spec.id === \"theokit-context\") continue;\n const paths = await resolvePathsForSpec(spec, opts.cwd, gitRoot);\n for (const path of paths) {\n if (seenReal.has(path)) continue;\n seenReal.add(path);\n const source = await loadOneSource(spec, path, opts, gitRoot);\n if (source !== undefined) out.push(source);\n }\n }\n return out;\n}\n\nasync function resolvePathsForSpec(\n spec: DiscoverySpec,\n cwd: string,\n gitRoot: string | undefined,\n): Promise<string[]> {\n if (spec.scope === \"cwd-only\") {\n return walkUpForFile(cwd, spec.pattern, cwd);\n }\n if (spec.scope === \"git-root-walk\") {\n return walkUpForFile(cwd, spec.pattern, gitRoot ?? cwd);\n }\n // globbed\n return walkUpForGlob(cwd, spec.pattern);\n}\n\nasync function loadOneSource(\n spec: DiscoverySpec,\n path: string,\n opts: DiscoveryRunnerOptions,\n gitRoot: string | undefined,\n): Promise<AggregatorSource | undefined> {\n // EC-E privacy: name uses relative-to-git-root for disambiguation;\n // NEVER absolute paths in the public `<source name=\"\">` attribute.\n // For cwd-only specs (THEO.md), no disambiguation is needed because\n // the scope can only ever match a single file.\n const relPath = relative(gitRoot ?? opts.cwd, dirname(path));\n const needsSuffix = spec.scope !== \"cwd-only\" && relPath !== \"\" && relPath !== \".\";\n const id = needsSuffix ? `${spec.id}@${relPath}` : spec.id;\n\n if (spec.parser === \"mdc\") {\n return loadMdcSource(spec, path, id, opts);\n }\n if (spec.parser === \"rules-frontmatter\") {\n return loadRulesSource(spec, path, id, opts);\n }\n if (spec.parser === \"frontmatter-zod\") {\n // Legacy `.theokit/context/*.md` — handled by `loadContextConfig` in\n // `context-manager.ts` for backward compat. We skip here unless caller\n // explicitly wants us to load it (currently always skipped).\n return undefined;\n }\n // plain-markdown\n return loadPlainMarkdownSource(spec, path, id, opts, gitRoot);\n}\n\n/**\n * Shared read → parse → activation-gate → source pipeline for the frontmatter\n * discovery parsers (mdc, rules). Binds the parser + the activation predicate so\n * the mdc and rules loaders differ only in those two, not in the surrounding\n * read/guard/shape boilerplate (DRY).\n */\nasync function loadParsedSource<F>(\n spec: DiscoverySpec,\n path: string,\n id: string,\n parse: (raw: string) => { frontmatter: F; body: string } | undefined,\n isActive: (frontmatter: F) => boolean,\n): Promise<AggregatorSource | undefined> {\n let raw: string;\n try {\n raw = await readFile(path, \"utf8\");\n } catch {\n return undefined;\n }\n const parsed = parse(raw);\n if (parsed === undefined || !isActive(parsed.frontmatter)) return undefined;\n return { id, source: path, content: parsed.body, priority: spec.priority, truncated: false };\n}\n\nfunction loadMdcSource(\n spec: DiscoverySpec,\n path: string,\n id: string,\n opts: DiscoveryRunnerOptions,\n): Promise<AggregatorSource | undefined> {\n return loadParsedSource(spec, path, id, parseMdc, (fm) =>\n shouldActivate(fm, opts.touchedFiles ?? []),\n );\n}\n\nfunction loadRulesSource(\n spec: DiscoverySpec,\n path: string,\n id: string,\n opts: DiscoveryRunnerOptions,\n): Promise<AggregatorSource | undefined> {\n return loadParsedSource(spec, path, id, parseRules, (fm) =>\n shouldActivateRule(fm, opts.touchedFiles ?? []),\n );\n}\n\nasync function loadPlainMarkdownSource(\n spec: DiscoverySpec,\n path: string,\n id: string,\n opts: DiscoveryRunnerOptions,\n gitRoot: string | undefined,\n): Promise<AggregatorSource | undefined> {\n const loaded = await loadPlainMarkdown(path, { maxBytesPerFile: opts.maxBytesPerFile });\n if (loaded === undefined) return undefined;\n let content = loaded.content;\n if (spec.followImports) {\n content = await resolveImports(content, path, {\n visited: new Set([path]),\n depth: 0,\n maxBytesPerFile: opts.maxBytesPerFile,\n // The document carrying the import is repository-controlled, so the repository is the\n // boundary it may not cross. Without this, `CLAUDE.md` / `GEMINI.md` — the two specs\n // with `followImports: true` — could name any absolute or `~/` path and have it\n // inlined into the system prompt.\n projectRoot: opts.importRoot ?? gitRoot ?? opts.cwd,\n });\n }\n return {\n id,\n source: path,\n content,\n priority: spec.priority,\n truncated: loaded.truncated,\n };\n}\n"]}
@@ -1,8 +1,6 @@
1
1
  import { atomicWriteJson } from './chunk-VF7EWVDG.js';
2
2
  import { diag } from './chunk-T7O6K6PX.js';
3
3
  import { readFile, rename } from 'fs/promises';
4
- import { homedir } from 'os';
5
- import { join } from 'path';
6
4
 
7
5
  function migrateSchema(opts) {
8
6
  const { db, currentVersion, migrations, label = "db" } = opts;
@@ -77,27 +75,7 @@ async function writeVersionedJson(path, data, currentVersion) {
77
75
  };
78
76
  await atomicWriteJson(path, file);
79
77
  }
80
- var THEOKIT_DIR_NAME = ".theokit";
81
- function getTheokitHome(cwd) {
82
- const override = process.env.THEOKIT_HOME?.trim();
83
- if (override !== void 0 && override.length > 0) {
84
- return override;
85
- }
86
- return join(cwd, THEOKIT_DIR_NAME);
87
- }
88
- function getProfilesRoot() {
89
- return join(homedir(), THEOKIT_DIR_NAME, "profiles");
90
- }
91
- function displayTheokitHome(cwd) {
92
- const resolved = getTheokitHome(cwd);
93
- const home = homedir();
94
- if (resolved === home) return "~";
95
- if (resolved.startsWith(`${home}/`)) {
96
- return `~${resolved.slice(home.length)}`;
97
- }
98
- return resolved;
99
- }
100
78
 
101
- export { displayTheokitHome, getProfilesRoot, getTheokitHome, migrateSchema, readVersionedJson, writeVersionedJson };
102
- //# sourceMappingURL=chunk-UPRJR6IP.js.map
103
- //# sourceMappingURL=chunk-UPRJR6IP.js.map
79
+ export { migrateSchema, readVersionedJson, writeVersionedJson };
80
+ //# sourceMappingURL=chunk-UCBJBJ27.js.map
81
+ //# sourceMappingURL=chunk-UCBJBJ27.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/internal/persistence/schema-version.ts"],"names":[],"mappings":"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","file":"chunk-UCBJBJ27.js","sourcesContent":["/**\n * Schema versioning helpers (ADR D62).\n *\n * Two parallel APIs:\n * - `migrateSchema` — SQLite via `PRAGMA user_version` + ordered forward-only migrations.\n * - `readVersionedJson` / `writeVersionedJson` — JSON files with `_schemaVersion` field.\n *\n * Forward-only: never deletes data, never downgrades. Migration callbacks\n * own the transformation; this module owns the bookkeeping.\n *\n * @internal\n */\n\nimport { readFile, rename } from \"node:fs/promises\";\nimport { diag } from \"../diagnostics.js\";\nimport { atomicWriteJson } from \"./atomic-write.js\";\n\n// ────────────────────── SQLite migrations ──────────────────────\n\n/**\n * The three `better-sqlite3` methods this module actually uses, declared structurally so nothing\n * here imports the driver.\n *\n * A real `better-sqlite3` `Database` satisfies it. `pragma` is called both to read\n * (`pragma(\"user_version\", { simple: true })`, which must return the number itself rather than a\n * row array) and to write (`pragma(\"user_version = N\")`). `transaction` must return a function\n * that, when called, runs the wrapped body inside a real SQLite transaction and rolls it back if\n * the body throws — a stub that merely calls through gives `migrateSchema` no atomicity, and a\n * half-applied migration is exactly what it exists to prevent.\n *\n * Semver-exempt: reachable via the `@theokit/sdk/internal/persistence` sub-path, which the package\n * declares in `exports` but does NOT cover with its semver contract.\n */\nexport interface SqliteLike {\n pragma: (statement: string, options?: { simple?: boolean }) => unknown;\n exec: (sql: string) => void;\n transaction: <Args extends unknown[], R>(fn: (...args: Args) => R) => (...args: Args) => R;\n}\n\n/**\n * One forward migration step.\n *\n * `up` runs inside the shared transaction and must be self-contained: it may not commit, and any\n * error it throws aborts the whole run and rolls back every step in it, including the ones that\n * already succeeded. `user_version` is set to `toVersion` immediately after `up` returns, inside\n * the same transaction, so version and schema move together or not at all.\n *\n * `toVersion` is the version the database reaches, not the one it starts from. Steps are ordered\n * by it, gaps are allowed, and two steps sharing a `toVersion` both run — nothing deduplicates\n * them.\n *\n * Semver-exempt: reachable via the `@theokit/sdk/internal/persistence` sub-path, which the package\n * declares in `exports` but does NOT cover with its semver contract.\n */\nexport interface Migration {\n toVersion: number;\n up: (db: SqliteLike) => void;\n}\n\n/**\n * Arguments to `migrateSchema`.\n *\n * `currentVersion` is the version the code expects, and it is the ceiling: a migration whose\n * `toVersion` exceeds it is left unapplied for a later release. `migrations` may be given in any\n * order and may contain steps already applied — both are filtered and sorted here. `label` only\n * ever appears in the downgrade error message.\n *\n * Semver-exempt: reachable via the `@theokit/sdk/internal/persistence` sub-path, which the package\n * declares in `exports` but does NOT cover with its semver contract.\n */\nexport interface MigrateSchemaOptions {\n db: SqliteLike;\n currentVersion: number;\n migrations: ReadonlyArray<Migration>;\n /** For log/error context (e.g., \"memory-index\", \"registry\"). */\n label?: string;\n}\n\n/**\n * What `migrateSchema` did.\n *\n * `from` is the `user_version` found on the database, `to` is the version it holds afterwards,\n * and `ran` is how many steps executed. When nothing was pending, `to === from` and `ran === 0`.\n *\n * `to` is the `toVersion` of the last step applied, NOT `currentVersion`. A database at version 3\n * against `currentVersion: 5` with no migration in that range comes back as\n * `{ from: 3, to: 3, ran: 0 }` — the gap is reported, not closed. Comparing `to` with\n * `currentVersion` is how a caller notices that the migration list is missing a step.\n *\n * Semver-exempt: reachable via the `@theokit/sdk/internal/persistence` sub-path, which the package\n * declares in `exports` but does NOT cover with its semver contract.\n */\nexport interface MigrateSchemaResult {\n from: number;\n to: number;\n ran: number;\n}\n\n/**\n * Bring a SQLite database's `user_version` up to `currentVersion` by running the steps that sit\n * between the two.\n *\n * Steps are sorted ascending by `toVersion` and filtered to `> stored && <= currentVersion`, then\n * run in that order inside ONE transaction: each `up` is followed immediately by the matching\n * `user_version` write. Either every step in the run commits or none does — a step that throws\n * aborts the transaction, propagates the error, and leaves the database at the version it started\n * from. There is no partially-migrated state and no resume point.\n *\n * Forward-only. When the stored version is greater than `currentVersion` — an older build opening\n * a database a newer one already migrated — this throws immediately, before touching anything,\n * with a message naming both versions and `label`. Nothing downgrades, and no data is deleted on\n * any path here; what a step does is the step's own business.\n *\n * A stored version equal to `currentVersion` returns without opening a transaction. A `user_version`\n * that is absent or not a number is read as 0, so a fresh database runs every step.\n *\n * Concurrency: the SQLite transaction is the only serialization. This takes no file lock, so two\n * processes migrating the same file at once are arbitrated by SQLite's own write lock — the loser\n * gets a busy or locked error from the driver, not a queued turn. Wrap the call in `withFileLock`\n * if that matters.\n *\n * Semver-exempt: reachable via the `@theokit/sdk/internal/persistence` sub-path, which the package\n * declares in `exports` but does NOT cover with its semver contract.\n */\nexport function migrateSchema(opts: MigrateSchemaOptions): MigrateSchemaResult {\n const { db, currentVersion, migrations, label = \"db\" } = opts;\n const storedRaw = db.pragma(\"user_version\", { simple: true });\n const stored = typeof storedRaw === \"number\" ? storedRaw : 0;\n\n if (stored > currentVersion) {\n throw new Error(\n `[${label}] schema version ${stored} > current ${currentVersion}; ` +\n \"did you downgrade the SDK? Forward-only migrations only.\",\n );\n }\n\n if (stored === currentVersion) {\n return { from: stored, to: stored, ran: 0 };\n }\n\n const pending = [...migrations]\n .sort((a, b) => a.toVersion - b.toVersion)\n .filter((m) => m.toVersion > stored && m.toVersion <= currentVersion);\n\n let ran = 0;\n let lastApplied = stored;\n\n db.transaction(() => {\n for (const m of pending) {\n m.up(db);\n db.pragma(`user_version = ${m.toVersion}`);\n lastApplied = m.toVersion;\n ran += 1;\n }\n })();\n\n return { from: stored, to: lastApplied, ran };\n}\n\n// ────────────────────── JSON versioned files ──────────────────────\n\n/**\n * The on-disk envelope: the payload under `data`, its schema version alongside it.\n *\n * `writeVersionedJson` always produces this shape. `readVersionedJson` reads it, but tolerates a\n * file that is not in it — a legacy object with no `_schemaVersion` is treated as version 0 and\n * handed to the migrate callback whole, so this type describes what is written, not everything\n * that can be read.\n *\n * Semver-exempt: reachable via the `@theokit/sdk/internal/persistence` sub-path, which the package\n * declares in `exports` but does NOT cover with its semver contract.\n */\nexport interface VersionedJsonFile<T> {\n _schemaVersion: number;\n data: T;\n}\n\n/**\n * Upgrade callback for `readVersionedJson`, invoked only when the stored version is BELOW the\n * current one.\n *\n * `parsed` is the whole parsed JSON value, not `parsed.data`. That is deliberate: a legacy file\n * predating the envelope has its fields at the top level, and a callback handed only `.data`\n * would receive `undefined` for exactly the files that need migrating. It is typed `unknown`\n * because nothing has validated it — narrow before reading.\n *\n * `fromVersion` is the `_schemaVersion` found on disk, or 0 when the field is missing or not a\n * number.\n *\n * The return value is used as-is and is NOT written back; the file on disk still holds the old\n * shape until someone calls `writeVersionedJson`. Throwing here propagates out of\n * `readVersionedJson`, which otherwise never throws — return the default value instead if an\n * unmigratable file should be survivable.\n *\n * Semver-exempt: reachable via the `@theokit/sdk/internal/persistence` sub-path, which the package\n * declares in `exports` but does NOT cover with its semver contract.\n */\nexport type VersionedJsonMigrate<T> = (parsed: unknown, fromVersion: number) => T;\n\n/**\n * Arguments to `readVersionedJson`.\n *\n * `defaultValue` is a factory rather than a value because it is called on several paths and each\n * caller must get its own object — returning a shared mutable default would let one caller's\n * edits appear in another's. It is called for a missing file, an unreadable one, a corrupt one,\n * a non-object payload, and a file written by a newer version.\n *\n * `migrate` is called only for a stored version strictly below `currentVersion`.\n *\n * Semver-exempt: reachable via the `@theokit/sdk/internal/persistence` sub-path, which the package\n * declares in `exports` but does NOT cover with its semver contract.\n */\nexport interface ReadVersionedJsonOptions<T> {\n path: string;\n currentVersion: number;\n migrate: VersionedJsonMigrate<T>;\n defaultValue: () => T;\n}\n\n/**\n * Read a versioned JSON file, migrating or falling back rather than failing.\n *\n * What comes back, by case:\n *\n * - version matches `currentVersion` — the file's `data` field, returned as `T` WITHOUT\n * validation. A file whose envelope is right but whose payload is not gets through.\n * - version below `currentVersion` — whatever `migrate(parsed, stored)` returns. The migrated\n * value is not persisted; the file is untouched.\n * - version above `currentVersion` — `defaultValue()`, plus a warning. Forward-only: a file\n * written by a newer build is left alone rather than downgraded.\n * - file missing, or unreadable for any other reason (permissions, a directory in its place) —\n * `defaultValue()`, silently. Absence and denial are not distinguished.\n * - file present but not valid JSON — `defaultValue()`, and the file is RENAMED to\n * `<path>.corrupt.<epoch-ms>` so the next run starts clean and the bad bytes stay available.\n * A rename that itself fails is warned about and the read still returns the default, which\n * means the same corrupt file will be met again next time.\n * - JSON that parses to a non-object (a number, a string, `null`) — `defaultValue()`, silently\n * and without moving the file aside.\n *\n * The only ways this throws are through the callbacks you supply: `migrate` or `defaultValue`\n * raising. Nothing else here rejects.\n *\n * There is no locking. A concurrent `writeVersionedJson` on the same path is atomic at the rename,\n * so a reader sees either the old file or the new one, never a half-written one — but a\n * read-modify-write built from this pair is NOT atomic across the two calls, and needs\n * `withFileLock` around both.\n *\n * Semver-exempt: reachable via the `@theokit/sdk/internal/persistence` sub-path, which the package\n * declares in `exports` but does NOT cover with its semver contract.\n */\nexport async function readVersionedJson<T>(opts: ReadVersionedJsonOptions<T>): Promise<T> {\n const { path, currentVersion, migrate, defaultValue } = opts;\n\n let raw: string;\n try {\n raw = await readFile(path, \"utf-8\");\n } catch {\n return defaultValue();\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n // T5.10 — move corrupt file aside so (a) user can investigate and\n // (b) next run starts fresh instead of hitting the same warning loop.\n const asidePath = `${path}.corrupt.${Date.now()}`;\n try {\n await rename(path, asidePath);\n diag(`[theokit-sdk] ${path} is corrupt; moved to ${asidePath}. Using default value.\\n`);\n } catch {\n diag(`[theokit-sdk] ${path} is corrupt; using default value.\\n`);\n }\n return defaultValue();\n }\n\n if (typeof parsed !== \"object\" || parsed === null) {\n return defaultValue();\n }\n\n const file = parsed as { _schemaVersion?: unknown; data?: unknown };\n const storedRaw = file._schemaVersion;\n const stored = typeof storedRaw === \"number\" ? storedRaw : 0;\n\n if (stored === currentVersion) {\n return file.data as T;\n }\n\n if (stored > currentVersion) {\n diag(\n `[theokit-sdk] ${path} schema version ${stored} > current ${currentVersion}; ` +\n \"using default value (forward-only).\\n\",\n );\n return defaultValue();\n }\n\n // EC-2 fix: pass the FULL parsed object to migrate, not just `file.data`.\n // Legacy shapes (e.g., `{ schemaVersion: \"1.0\", agents: {...} }` without\n // a `data` field) need to inspect the whole thing.\n return migrate(parsed, stored);\n}\n\n/**\n * Write `data` wrapped in the version envelope, replacing the file atomically.\n *\n * The bytes go to a temp file in the same directory and are moved into place with `rename`, so a\n * crash or a concurrent reader never observes a partial file: the path holds either the previous\n * content or the complete new content. Parent directories are created as needed, and the file is\n * created with mode 0600 (subject to the process umask), formatted with two-space indentation and\n * a trailing newline.\n *\n * `currentVersion` is stamped verbatim as `_schemaVersion`; nothing checks it against what was\n * already on disk, so this will happily overwrite a newer file with an older version. Read first\n * if that matters.\n *\n * Rejects when `data` cannot be serialized — a circular reference, or a `toJSON` that throws.\n * Serialization happens before any filesystem call, so on that path no directory is created, no\n * temp file exists, and the file already on disk is untouched.\n *\n * Atomicity is the rename's, so it is as strong as the filesystem's: on NFS, SMB or FUSE the\n * underlying writer emits a one-shot warning that the guarantee is weaker there.\n *\n * Semver-exempt: reachable via the `@theokit/sdk/internal/persistence` sub-path, which the package\n * declares in `exports` but does NOT cover with its semver contract.\n */\nexport async function writeVersionedJson<T>(\n path: string,\n data: T,\n currentVersion: number,\n): Promise<void> {\n const file: VersionedJsonFile<T> = {\n _schemaVersion: currentVersion,\n data,\n };\n await atomicWriteJson(path, file);\n}\n"]}
@@ -0,0 +1,35 @@
1
+ import { homedir } from 'os';
2
+ import { join } from 'path';
3
+
4
+ // src/internal/persistence/paths.ts
5
+ var THEOKIT_DIR_LITERAL = ".theokit";
6
+ var CLAUDE_DIR_NAME = ".claude";
7
+ function getTheokitHome(cwd) {
8
+ const override = process.env.THEOKIT_HOME?.trim();
9
+ if (override !== void 0 && override.length > 0) {
10
+ return override;
11
+ }
12
+ return join(cwd, THEOKIT_DIR_LITERAL);
13
+ }
14
+ function projectConfigRoots(cwd) {
15
+ return [join(cwd, THEOKIT_DIR_LITERAL), join(cwd, CLAUDE_DIR_NAME)];
16
+ }
17
+ function pluginBundleRoots(cwd) {
18
+ return projectConfigRoots(cwd).map((root) => join(root, "plugins"));
19
+ }
20
+ function getProfilesRoot() {
21
+ return join(homedir(), THEOKIT_DIR_LITERAL, "profiles");
22
+ }
23
+ function displayTheokitHome(cwd) {
24
+ const resolved = getTheokitHome(cwd);
25
+ const home = homedir();
26
+ if (resolved === home) return "~";
27
+ if (resolved.startsWith(`${home}/`)) {
28
+ return `~${resolved.slice(home.length)}`;
29
+ }
30
+ return resolved;
31
+ }
32
+
33
+ export { displayTheokitHome, getProfilesRoot, getTheokitHome, pluginBundleRoots, projectConfigRoots };
34
+ //# sourceMappingURL=chunk-UFLD2HEV.js.map
35
+ //# sourceMappingURL=chunk-UFLD2HEV.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/internal/persistence/paths.ts"],"names":[],"mappings":";;;;AAuCA,IAAM,mBAAA,GAAsB,UAAA;AAG5B,IAAM,eAAA,GAAkB,SAAA;AAuBjB,SAAS,eAAe,GAAA,EAAqB;AAClD,EAAA,MAAM,QAAA,GAAW,OAAA,CAAQ,GAAA,CAAI,YAAA,EAAc,IAAA,EAAK;AAChD,EAAA,IAAI,QAAA,KAAa,MAAA,IAAa,QAAA,CAAS,MAAA,GAAS,CAAA,EAAG;AACjD,IAAA,OAAO,QAAA;AAAA,EACT;AACA,EAAA,OAAO,IAAA,CAAK,KAAK,mBAAmB,CAAA;AACtC;AA6BO,SAAS,mBAAmB,GAAA,EAAuB;AACxD,EAAA,OAAO,CAAC,KAAK,GAAA,EAAK,mBAAmB,GAAG,IAAA,CAAK,GAAA,EAAK,eAAe,CAAC,CAAA;AACpE;AAeO,SAAS,kBAAkB,GAAA,EAAuB;AACvD,EAAA,OAAO,kBAAA,CAAmB,GAAG,CAAA,CAAE,GAAA,CAAI,CAAC,IAAA,KAAS,IAAA,CAAK,IAAA,EAAM,SAAS,CAAC,CAAA;AACpE;AAeO,SAAS,eAAA,GAA0B;AACxC,EAAA,OAAO,IAAA,CAAK,OAAA,EAAQ,EAAG,mBAAA,EAAqB,UAAU,CAAA;AACxD;AAmBO,SAAS,mBAAmB,GAAA,EAAqB;AACtD,EAAA,MAAM,QAAA,GAAW,eAAe,GAAG,CAAA;AACnC,EAAA,MAAM,OAAO,OAAA,EAAQ;AACrB,EAAA,IAAI,QAAA,KAAa,MAAM,OAAO,GAAA;AAC9B,EAAA,IAAI,QAAA,CAAS,UAAA,CAAW,CAAA,EAAG,IAAI,GAAG,CAAA,EAAG;AACnC,IAAA,OAAO,CAAA,CAAA,EAAI,QAAA,CAAS,KAAA,CAAM,IAAA,CAAK,MAAM,CAAC,CAAA,CAAA;AAAA,EACxC;AACA,EAAA,OAAO,QAAA;AACT","file":"chunk-UFLD2HEV.js","sourcesContent":["/**\n * Path resolution for SDK state files (ADR D60).\n *\n * Theokit anchors state at `<cwd>/.theokit/` by default (per-cwd). An\n * optional `THEOKIT_HOME` environment variable overrides this, enabling\n * test isolation, profile switching, and multi-tenant deployments.\n *\n * Rules:\n * - `getTheokitHome(cwd)` is the canonical resolver **for cwd-anchored state**. Never hardcode\n * `path.join(cwd, \".theokit\")` in callers — use this function so tests\n * and overrides stay consistent.\n *\n * M94 — this comment said \"the ONLY canonical resolver\", and stopped being true: the\n * transcript gained `transcriptRoot()`, which is **home-anchored** (`~/.theokit`) with the same\n * `THEOKIT_HOME` override. The two defaults differ on purpose — unifying would move the\n * transcript of everyone who does NOT set the variable, which is a data migration and not a\n * re-export.\n *\n * A consequence worth writing down: **without `THEOKIT_HOME` the state stays split in two**\n * — registry in `<cwd>/.theokit`, transcript in `~/.theokit`. M94 unifies only for those who set\n * the variable. Unifying both defaults is another milestone's work.\n * - `getProfilesRoot()` is intentionally home-anchored (not affected by\n * `THEOKIT_HOME`) so `theokit profile list` discovers all profiles\n * regardless of which is active.\n * - `displayTheokitHome(cwd)` returns a human-readable path for logs.\n *\n * @internal\n */\n\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\n\n/**\n * The project config directory literal.\n *\n * Renamed from `THEOKIT_DIR_NAME` in #410. Sharing a name with the (now removed) sovereign env var\n * was the MECHANISM of that defect, not scenery: every grep for the variable landed on this const\n * and looked answered, so \"is it read?\" returned five hits and nobody checked what they were.\n */\nconst THEOKIT_DIR_LITERAL = \".theokit\";\n\n/** The Claude Code CLI's project configuration directory. */\nconst CLAUDE_DIR_NAME = \".claude\";\n\n/**\n * Resolve the directory cwd-anchored SDK state lives in.\n *\n * `THEOKIT_HOME` wins when it is set and not blank after trimming; the trimmed value is used, and\n * it is used VERBATIM — it is not resolved against `cwd`, so a relative value stays relative and\n * `.theokit` is not appended to it. Otherwise the answer is `<cwd>/.theokit`.\n *\n * The environment is read on every call, so a change to the variable takes effect immediately\n * rather than being frozen at import.\n *\n * This creates nothing and checks nothing: the returned path may not exist, and the caller owns\n * the `mkdir`. Call it instead of writing `join(cwd, \".theokit\")` by hand, or the override stops\n * working for that one call site and tests silently touch the real home.\n *\n * Not the whole story about where state lives — the transcript is home-anchored via\n * `transcriptRoot()`, honoring the same variable but defaulting to `~/.theokit`. With\n * `THEOKIT_HOME` unset, state is genuinely split between two roots.\n *\n * Semver-exempt: reachable via the `@theokit/sdk/internal/persistence` sub-path, which the package\n * declares in `exports` but does NOT cover with its semver contract.\n */\nexport function getTheokitHome(cwd: string): string {\n const override = process.env.THEOKIT_HOME?.trim();\n if (override !== undefined && override.length > 0) {\n return override;\n }\n return join(cwd, THEOKIT_DIR_LITERAL);\n}\n\n/**\n * Every directory a project's configuration may be read from, in precedence order.\n *\n * `.theokit` first, then `.claude`. The order is the whole contract: a project that declares a\n * skill, agent or rule in both means the explicit namespace to win, and a caller merging these\n * roots must therefore keep the FIRST occurrence of a name rather than the last.\n *\n * `.claude` is read because the formats already agree and only the location did not. Measured\n * 2026-08-26: the SKILL.md frontmatter this SDK requires (`name` + `description`) is exactly what\n * the CLI writes, its hook config is the same JSON shape, and 59 of the CLI's agent declarations\n * parse here unchanged. A repository set up for the CLI was failing on the directory name alone.\n *\n * NOT a rename of `.theokit`, and not a migration. Both are read, so nothing that works today stops\n * working — which is why this returns a LIST and not a single resolved answer.\n *\n * Deliberately NOT affected by `THEOKIT_HOME`, and this is the one thing to remember about it.\n * That variable relocates cwd-anchored SDK *state* — sessions, the credential store. A project's\n * *configuration* is a property of the repository, not of where this SDK keeps its state, and the\n * loaders that read these directories have always anchored on `cwd` directly. Honouring the\n * override here would silently move where a project's agents and skills come from, which is a\n * behaviour change wearing the costume of a refactor.\n *\n * Creates nothing and checks nothing; either path may not exist, and the caller owns that.\n *\n * Semver-exempt: reachable via the `@theokit/sdk/internal/persistence` sub-path, which the package\n * declares in `exports` but does NOT cover with its semver contract.\n */\nexport function projectConfigRoots(cwd: string): string[] {\n return [join(cwd, THEOKIT_DIR_LITERAL), join(cwd, CLAUDE_DIR_NAME)];\n}\n\n/**\n * Every directory that may hold a plugin BUNDLE contributed by the Claude Code CLI.\n *\n * A CLI plugin is not a JS entry point — it is a folder whose `skills/` and `agents/` are what it\n * exists to provide. Measured 2026-08-26 on an installed one: seven agents and three skills beside\n * a manifest in `.claude-plugin/plugin.json`. Parsing that manifest and stopping there produced a\n * plugin that loaded and did nothing.\n *\n * Project-scoped deliberately. The CLI also keeps plugins under `~/.claude/plugins/cache`, behind\n * its own installer and enable/disable state — reproducing that is an installation system, not\n * reading a project's configuration, and guessing at someone's enablement would run code they\n * turned off.\n */\nexport function pluginBundleRoots(cwd: string): string[] {\n return projectConfigRoots(cwd).map((root) => join(root, \"plugins\"));\n}\n\n/**\n * The directory holding every profile: always `~/.theokit/profiles`, from `os.homedir()`.\n *\n * Deliberately NOT affected by `THEOKIT_HOME`, which is the one thing to remember about it. If it\n * followed the override, a session pointed at one profile would only be able to see that profile,\n * and `theokit profile list` could never enumerate the rest. Profiles are the thing the override\n * switches between, so their index cannot live behind it.\n *\n * Takes no `cwd` for the same reason. Creates nothing; the path may not exist.\n *\n * Semver-exempt: reachable via the `@theokit/sdk/internal/persistence` sub-path, which the package\n * declares in `exports` but does NOT cover with its semver contract.\n */\nexport function getProfilesRoot(): string {\n return join(homedir(), THEOKIT_DIR_LITERAL, \"profiles\");\n}\n\n/**\n * The same path `getTheokitHome(cwd)` returns, shortened for display: the home directory prefix\n * collapses to `~`, so `/home/ada/.theokit` prints as `~/.theokit`.\n *\n * For humans only — log lines, CLI output, error messages. The result is NOT a usable path: `~`\n * is a shell convention that `fs` does not expand, so passing this to a filesystem call resolves\n * a literal directory named `~` relative to the process cwd. Use `getTheokitHome` for anything\n * that touches disk.\n *\n * Collapsing is a prefix match on the home directory followed by a literal `/`, so a sibling like\n * `/home/adalovelace` is left alone even though `/home/ada` is a string prefix of it. A path\n * outside the home directory comes back unchanged — and so does a Windows path, where the\n * separator is a backslash and the prefix test therefore never matches.\n *\n * Semver-exempt: reachable via the `@theokit/sdk/internal/persistence` sub-path, which the package\n * declares in `exports` but does NOT cover with its semver contract.\n */\nexport function displayTheokitHome(cwd: string): string {\n const resolved = getTheokitHome(cwd);\n const home = homedir();\n if (resolved === home) return \"~\";\n if (resolved.startsWith(`${home}/`)) {\n return `~${resolved.slice(home.length)}`;\n }\n return resolved;\n}\n"]}
@@ -44,6 +44,27 @@ var DEFAULT_DISCOVERY_SPECS = [
44
44
  followImports: false,
45
45
  priority: 45
46
46
  },
47
+ {
48
+ // Rules written for the Claude Code CLI. Measured 2026-08-26 over this repository's 32 rule
49
+ // files: none carries frontmatter, and `rules-frontmatter` already reads a file without it as
50
+ // `alwaysApply: true` — the format needed nothing, only a spec pointing at the directory.
51
+ //
52
+ // 47, not 46. Specs sort ascending and a context budget drops the tail first, so it must land
53
+ // AFTER `.theokit/rules` (45) — the explicit namespace should survive a squeeze the borrowed one
54
+ // does not. It must also leave a slot on BOTH sides: B-127 makes these numbers a public contract
55
+ // precisely so a consumer can place its own source between two defaults, and 46 would have left
56
+ // no room between 45 and itself. 47 keeps 46 free below and 48–49 free above.
57
+ //
58
+ // The reckoning B-127's docblock asks for: no published priority MOVES, so a consumer that chose
59
+ // 46, 48 or 49 is unaffected. A consumer that had chosen 47 now collides — that is the cost of
60
+ // an eighth default, paid once and recorded here rather than discovered later.
61
+ id: "claude-rules",
62
+ pattern: ".claude/rules/*.md",
63
+ scope: "globbed",
64
+ parser: "rules-frontmatter",
65
+ followImports: false,
66
+ priority: 47
67
+ },
47
68
  {
48
69
  id: "theokit-context",
49
70
  pattern: ".theokit/context/*.md",
@@ -124,5 +145,5 @@ async function walkUpForGlob(cwd, pattern) {
124
145
  }
125
146
 
126
147
  export { DEFAULT_DISCOVERY_SPECS, findGitRoot, isSafePattern, walkUpForFile, walkUpForGlob };
127
- //# sourceMappingURL=chunk-GTKFV7O5.js.map
128
- //# sourceMappingURL=chunk-GTKFV7O5.js.map
148
+ //# sourceMappingURL=chunk-WFC26L6Y.js.map
149
+ //# sourceMappingURL=chunk-WFC26L6Y.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/internal/runtime/context/context-discovery.ts"],"names":[],"mappings":"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","file":"chunk-WFC26L6Y.js","sourcesContent":["/**\n * Context file discovery (T1.1, ADRs D150 / D151).\n *\n * Discovers context files via three scopes:\n * - `cwd-only` — single dir, single path lookup\n * - `git-root-walk` — walk cwd → git-root, collect every directory's match\n * (nearest-first ordering)\n * - `globbed` — glob pattern relative to cwd (e.g. `.cursor/rules/*.mdc`)\n *\n * Pure `existsSync` checks — **no `.gitignore` parsing** (EC-A, KISS) and\n * **no invented `.theokitignore`** (EC-B). Paths normalized via\n * `realpath` to dedup symlink chains pointing to the same physical file\n * (EC-F). Git worktrees work transparently because `.git` exists as a\n * file in that case (EC-N).\n *\n * @internal\n */\n\nimport { existsSync, realpathSync } from \"node:fs\";\nimport { glob } from \"node:fs/promises\";\nimport { dirname, isAbsolute, join, resolve } from \"node:path\";\n\n/** Single filename (\"AGENTS.md\") or relative glob (\".cursor/rules/*.mdc\"). */\nexport type DiscoveryScope = \"cwd-only\" | \"git-root-walk\" | \"globbed\";\n\n/** Parser to apply once file is read. */\nexport type DiscoveryParser = \"plain-markdown\" | \"mdc\" | \"frontmatter-zod\" | \"rules-frontmatter\";\n\n/**\n * One kind of context file the runner knows how to find and read. The shipped registry is\n * `DEFAULT_DISCOVERY_SPECS`; a caller supplies its own array to change the set.\n *\n * `scope` decides how `pattern` is used and how many files a single spec can yield:\n * `cwd-only` looks for one path and stops, `git-root-walk` collects a match in every directory\n * from `cwd` up to the git root (nearest first), and `globbed` expands `pattern` as a glob\n * relative to `cwd`. So `pattern` is a filename for the first two and a glob for the third —\n * putting a glob on a walk scope silently finds nothing.\n *\n * `priority` orders the merged prompt, ascending, and is a plain number rather than an index, so\n * a new spec can be slotted between two existing ones. Lower means earlier and therefore more\n * general; later content wins on conflict.\n *\n * `parser` must match the file format — `plain-markdown` reads the whole file, `mdc` and\n * `rules-frontmatter` parse frontmatter and can DECLINE the file when its activation conditions\n * do not hold, and `frontmatter-zod` is the legacy path the runner currently skips entirely.\n *\n * `followImports` is honored only by `plain-markdown`, and turns `@path` directives in the body\n * into inlined content bounded by the import root. Setting it on a frontmatter parser does\n * nothing.\n *\n * `id` names the source in `<source name=\"\">` and in telemetry. When one spec matches files in\n * several directories, the runner suffixes it with the path relative to the git root to keep them\n * apart.\n *\n * @public — re-exported from `@theokit/sdk/context`, and therefore under semver.\n */\nexport interface DiscoverySpec {\n /** Stable identifier — used as `<source name=\"\">` and telemetry key. */\n readonly id: string;\n /** Priority for merge (lower = earlier in prompt). */\n readonly priority: number;\n /** Filename (cwd-only/git-root-walk) or glob (globbed). */\n readonly pattern: string;\n readonly scope: DiscoveryScope;\n readonly parser: DiscoveryParser;\n /** Whether to follow `@path` import directives (CLAUDE.md / GEMINI.md). */\n readonly followImports: boolean;\n}\n\n/**\n * The context files theokit looks for out of the box, in the order they are concatenated.\n *\n * Two things follow from the ordering. `AGENTS.md` comes first at priority 10 and `THEO.md` last,\n * so theokit-specific instruction wins over the vendor-neutral file on conflict. And the array is\n * consumed in the order written — the runner does not re-sort it — so a caller passing its own\n * array is responsible for keeping `priority` and array position consistent.\n *\n * `CLAUDE.md` and `GEMINI.md` are the only two entries with `followImports: true`, which means\n * they are the only files whose `@path` directives pull other files into the prompt. Those\n * imports cannot escape the import root.\n *\n * Frozen only by type: `ReadonlyArray` is a compile-time constraint, and the array and its\n * elements are not deep-frozen at runtime. Build a new array rather than mutating this one.\n *\n * @public — re-exported from `@theokit/sdk/context`, and therefore under semver.\n */\nexport const DEFAULT_DISCOVERY_SPECS: ReadonlyArray<DiscoverySpec> = [\n {\n id: \"AGENTS.md\",\n pattern: \"AGENTS.md\",\n scope: \"git-root-walk\",\n parser: \"plain-markdown\",\n followImports: false,\n priority: 10,\n },\n {\n id: \"GEMINI.md\",\n pattern: \"GEMINI.md\",\n scope: \"git-root-walk\",\n parser: \"plain-markdown\",\n followImports: true,\n priority: 20,\n },\n {\n id: \"CLAUDE.md\",\n pattern: \"CLAUDE.md\",\n scope: \"git-root-walk\",\n parser: \"plain-markdown\",\n followImports: true,\n priority: 30,\n },\n {\n id: \"cursor-rules\",\n pattern: \".cursor/rules/*.mdc\",\n scope: \"globbed\",\n parser: \"mdc\",\n followImports: false,\n priority: 40,\n },\n {\n id: \"theokit-rules\",\n pattern: \".theokit/rules/*.md\",\n scope: \"globbed\",\n parser: \"rules-frontmatter\",\n followImports: false,\n priority: 45,\n },\n {\n // Rules written for the Claude Code CLI. Measured 2026-08-26 over this repository's 32 rule\n // files: none carries frontmatter, and `rules-frontmatter` already reads a file without it as\n // `alwaysApply: true` — the format needed nothing, only a spec pointing at the directory.\n //\n // 47, not 46. Specs sort ascending and a context budget drops the tail first, so it must land\n // AFTER `.theokit/rules` (45) — the explicit namespace should survive a squeeze the borrowed one\n // does not. It must also leave a slot on BOTH sides: B-127 makes these numbers a public contract\n // precisely so a consumer can place its own source between two defaults, and 46 would have left\n // no room between 45 and itself. 47 keeps 46 free below and 48–49 free above.\n //\n // The reckoning B-127's docblock asks for: no published priority MOVES, so a consumer that chose\n // 46, 48 or 49 is unaffected. A consumer that had chosen 47 now collides — that is the cost of\n // an eighth default, paid once and recorded here rather than discovered later.\n id: \"claude-rules\",\n pattern: \".claude/rules/*.md\",\n scope: \"globbed\",\n parser: \"rules-frontmatter\",\n followImports: false,\n priority: 47,\n },\n {\n id: \"theokit-context\",\n pattern: \".theokit/context/*.md\",\n scope: \"globbed\",\n parser: \"frontmatter-zod\",\n followImports: false,\n priority: 50,\n },\n {\n id: \"THEO.md\",\n pattern: \".theokit/THEO.md\",\n scope: \"cwd-only\",\n parser: \"plain-markdown\",\n followImports: false,\n priority: 60,\n },\n];\n\nconst SAFE_FILENAME = /^[a-zA-Z0-9_.\\-/*]+$/;\nconst TRAVERSAL_RE = /(^|\\/)\\.\\.(\\/|$)/;\n\n/**\n * Reject patterns that contain path traversal (`..`) or non-allowed\n * characters (D81 parity, EC-4).\n *\n * @internal\n */\nexport function isSafePattern(pattern: string): boolean {\n if (typeof pattern !== \"string\" || pattern.length === 0) return false;\n if (TRAVERSAL_RE.test(pattern)) return false;\n if (isAbsolute(pattern)) return false;\n return SAFE_FILENAME.test(pattern);\n}\n\n/**\n * Walk upward from `cwd` looking for the closest directory containing\n * a `.git` entry (file OR directory — worktrees use a `.git` FILE,\n * EC-N). Returns the absolute path of that directory, or `undefined`\n * when no git root exists at or above `cwd`.\n *\n * @internal\n */\nexport function findGitRoot(cwd: string): string | undefined {\n if (typeof cwd !== \"string\" || cwd.length === 0) return undefined;\n let current = resolve(cwd);\n // Guard against infinite loops on weird filesystems.\n for (let i = 0; i < 64; i += 1) {\n if (existsSync(join(current, \".git\"))) return current;\n const parent = dirname(current);\n if (parent === current) return undefined;\n current = parent;\n }\n return undefined;\n}\n\n/**\n * Walk `cwd` upward to `stopDir` (inclusive) collecting every existing\n * occurrence of `filename`. Returns absolute, realpath-deduped paths in\n * nearest-first order (innermost dir first).\n *\n * No `.gitignore` parsing (EC-A). Realpath collapses symlink chains\n * pointing to the same physical file (EC-F). Filesystem races (file\n * deleted mid-walk) are skipped silently (EC-5).\n *\n * @internal\n */\n// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: walk-up loop combines validation + realpath dedup + FS-race handling + stopDir guard in a single bounded loop; splitting fragments the dedup invariant.\nexport function walkUpForFile(\n cwd: string,\n filename: string,\n stopDir: string | undefined,\n): string[] {\n if (!isSafePattern(filename)) {\n return [];\n }\n const start = resolve(cwd);\n const stop = stopDir !== undefined ? resolve(stopDir) : undefined;\n const found: string[] = [];\n const seenReal = new Set<string>();\n let current = start;\n // 64-level depth cap.\n for (let i = 0; i < 64; i += 1) {\n const candidate = join(current, filename);\n if (existsSync(candidate)) {\n let real: string;\n try {\n real = realpathSync(candidate);\n } catch {\n // FS race (deleted mid-walk) — skip.\n real = candidate;\n }\n if (!seenReal.has(real)) {\n seenReal.add(real);\n found.push(real);\n }\n }\n if (stop !== undefined && current === stop) break;\n const parent = dirname(current);\n if (parent === current) break;\n current = parent;\n }\n return found;\n}\n\n/**\n * Glob-style discovery under `cwd` (e.g. `.cursor/rules/*.mdc`, `.theokit/rules/**\\/*.md`).\n * Returns absolute, lex-sorted paths.\n *\n * `*` matches within one path segment and `**` spans any depth, including zero — so\n * `.theokit/rules/**\\/*.md` finds `rules/top.md` as well as `rules/deep/nested/inner.md`, while\n * `.theokit/rules/*.md` keeps its flat meaning and finds only the first. That distinction is the\n * compatibility contract: every existing spec uses a single `*`, and widening it would silently\n * start absorbing nested files nobody chose to expose.\n *\n * ## Why this used to be flat, and what changed (B-119)\n *\n * The previous implementation split the pattern at its LAST `/`, treated the prefix as a literal\n * directory and did one `readdir` — documented as \"nested directories deferred to v2\" (EC-R). The\n * deferral was deliberate; what made it a defect was measured from a consumer. TheoCode's own rule\n * loader descends recursively, so migrating it onto the `theokit-rules` spec would have silently\n * dropped every nested rule — on the path that decides whether a repository's hooks execute. And a\n * pattern written to say so, `.theokit/rules/**\\/*.md`, resolved its directory part to a literal\n * `**` and matched NOTHING, not even the top-level file it matched before the globstar was added.\n *\n * ## Why the stdlib rather than a walker\n *\n * `fs.promises.glob` (Node ≥ 22, and this package requires ≥ 22.12) implements exactly these\n * semantics, verified against a fixture before adoption: `**\\/*.md` returns all three depths,\n * `*.md` returns one, and it emits no experimental warning. Writing a recursive walker here would\n * have been a third implementation of matching inside one package — the same duplication that let\n * the enumerator and the compiler in `context-glob.ts` disagree in the first place. `globToRegex`\n * stays where it belongs: deciding whether a rule APPLIES to a set of paths, which is a different\n * question from which files exist.\n *\n * `isSafePattern` still runs first and is unchanged, so `..` is refused before any I/O.\n *\n * @internal\n */\nexport async function walkUpForGlob(cwd: string, pattern: string): Promise<string[]> {\n if (!isSafePattern(pattern)) return [];\n const found: string[] = [];\n try {\n for await (const entry of glob(pattern, { cwd })) {\n found.push(resolve(cwd, entry));\n }\n } catch {\n // A pattern whose directory does not exist is the ordinary case — most projects have no\n // `.cursor/rules/`. Same outcome as matching nothing.\n return [];\n }\n // Sorted, because discovery order becomes prompt order and must not vary with the filesystem.\n //\n // The comparator is explicit and deliberately NOT `localeCompare`, which is the usual suggestion\n // for a bare `.sort()`. `localeCompare` orders by the machine's locale, so the same tree would\n // assemble a different prompt on a differently-configured machine — trading one source of\n // non-determinism for a subtler one. Code-unit ordering is what a bare `.sort()` already does for\n // strings; writing it out states the intent and keeps the result machine-independent.\n return found.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));\n}\n"]}
@@ -1,5 +1,5 @@
1
1
  import { sessions, transcriptKey, hydratedKeys } from './chunk-WTMU7J4U.js';
2
- import { persistTurn, readSessionMessages } from './chunk-BMAZLQQ4.js';
2
+ import { persistTurn, readSessionMessages } from './chunk-G24KZRSA.js';
3
3
  import { diag } from './chunk-T7O6K6PX.js';
4
4
 
5
5
  // src/internal/session/agent-session.ts
@@ -46,7 +46,7 @@ function persistTurnToTranscript(store, loc, sessionId, turn, onCompact) {
46
46
  const count = (recordCounts.get(key) ?? 0) + 1;
47
47
  recordCounts.set(key, count);
48
48
  if (turn.autoCompact !== void 0) {
49
- const { autoCompactIfNeeded } = await import('./compact-session-FUJDVF7B.js');
49
+ const { autoCompactIfNeeded } = await import('./compact-session-QGDNP45U.js');
50
50
  const fired = await autoCompactIfNeeded({
51
51
  store,
52
52
  loc,
@@ -105,5 +105,5 @@ function enqueueSessionWrite(cwd, agentId, fn) {
105
105
  }
106
106
 
107
107
  export { appendSessionMessage, clearSession, discardSession, enqueueSessionWrite, flushSessionWrites, getSessionMessages, hydrateSession, persistTurnToTranscript };
108
- //# sourceMappingURL=chunk-OHBIUXKM.js.map
109
- //# sourceMappingURL=chunk-OHBIUXKM.js.map
108
+ //# sourceMappingURL=chunk-WG7R5W6R.js.map
109
+ //# sourceMappingURL=chunk-WG7R5W6R.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/internal/session/agent-session.ts"],"names":[],"mappings":"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","file":"chunk-OHBIUXKM.js","sourcesContent":["import type { SessionStore } from \"../../types/session-store.js\";\nimport {\n type PersistTurnInput,\n persistTurn,\n readSessionMessages,\n type TranscriptLocation,\n} from \"./agent-session-store.js\";\n\n/**\n * Per-agent conversation history kept across runs (and across `Agent.resume()`\n * within the same process). Lets the fixture responder recall prior facts when\n * the user asks a follow-up question.\n *\n * SE40 — disk persistence IS the native Claude-shaped `.jsonl` transcript. The\n * in-memory cache holds the narrowed `SessionMessage[]` (user/assistant text) so\n * a send can read `priorMessages` synchronously; the whole rich turn (user +\n * assistant + tool blocks) lands on disk once per send via {@link persistTurnToTranscript}\n * (fed by `run.conversation()`). Hydration on resume reconstructs the session by\n * walking the transcript DAG (`readSessionMessages`).\n *\n * @internal\n */\n\n// `SessionMessage` lives in `./session-types.ts` (leaf types file). Re-exported\n// for back-compat with downstream importers that pulled it from here.\nexport type { SessionMessage } from \"./session-types.js\";\n\nimport type { SessionMessage } from \"./session-types.js\";\n\n// M75 — the cache lives in a leaf; see session-cache.ts for the reason (cycle broken by extraction).\nexport {\n hydratedKeys,\n invalidateSessionCache,\n sessions,\n transcriptKey,\n} from \"./session-cache.js\";\n\nimport { diag } from \"../diagnostics.js\";\nimport { hydratedKeys, sessions, transcriptKey } from \"./session-cache.js\";\n\nconst pendingWrites = new Map<string, Promise<void>>();\nconst recordCounts = new Map<string, number>();\n\n/**\n * Append a session message to the in-memory cache only. Disk persistence for the\n * whole turn happens once per send via {@link persistTurnToTranscript}; the cache\n * feeds `priorMessages` / `onBeforeSend.previousMessageCount` synchronously.\n *\n * @internal\n */\nexport function appendSessionMessage(agentId: string, message: SessionMessage): void {\n const existing = sessions.get(agentId) ?? [];\n existing.push(message);\n // `delete` + `set` reinserts at the END: a JS `Map` preserves insertion order, so the first key\n // is always the least recently touched. That is the entire LRU, with no new structure (parsimony\n // rungs 2/5 — the ordering we need is already a language guarantee).\n sessions.delete(agentId);\n sessions.set(agentId, existing);\n enforceCeiling();\n}\n\n/**\n * Ceiling on sessions kept in memory.\n *\n * The runtime only reads the **active** session; the rest is pure cache, rebuildable from the\n * on-disk transcript. 32 is deliberately generous — the primary removal path is the explicit\n * `discardSession()` at the end of the agent's life, and this ceiling is a safety net against a\n * long-lived process running hundreds of sessions (plan risk #2: a tight ceiling could evict a\n * session still referenced by an in-flight async path).\n */\nexport const MAX_CACHED_SESSIONS = 32;\n\nfunction enforceCeiling(): void {\n while (sessions.size > MAX_CACHED_SESSIONS) {\n const oldest = sessions.keys().next().value;\n if (oldest === undefined) return;\n sessions.delete(oldest);\n forgetBookkeeping(oldest);\n }\n}\n\n/**\n * Erases the bookkeeping for an `agentId` across EVERY `cwd` it appears in.\n *\n * The three maps are keyed by `transcriptKey(cwd, agentId)`; `sessions` is the only one keyed by the\n * raw `agentId`. The ceiling's first version deleted from all three using `sessions`' key — that is,\n * deleted nothing — and left `hydratedKeys` **orphaned**. Since `hydrateSession` returns early when\n * the marker is present, an evicted session came back **empty** instead of rehydrating from disk:\n * silent amnesia, and a regression new to M95, because before it nothing evicted.\n *\n * The ceiling only knows the `agentId`, not the `cwd`, so it scans by suffix — which is the format\n * `transcriptKey` produces. A scan rather than an index because these maps hold tens of entries, not\n * thousands: a reverse index here would be new structure for a problem that does not exist.\n */\nfunction forgetBookkeeping(agentId: string): void {\n const suffix = transcriptKey(\"\", agentId).slice(0 - agentId.length - 2);\n for (const k of [...hydratedKeys]) if (k.endsWith(suffix)) hydratedKeys.delete(k);\n for (const k of [...pendingWrites.keys()]) if (k.endsWith(suffix)) pendingWrites.delete(k);\n for (const k of [...recordCounts.keys()]) if (k.endsWith(suffix)) recordCounts.delete(k);\n}\n\n/**\n * Erases the agent's module bookkeeping and returns how many entries were removed.\n *\n * M95 — `invalidateSessionCache` used to clear **two** of the four maps (`sessions`, `hydratedKeys`);\n * `pendingWrites` and `recordCounts` were never touched by id, so they grew for the life of the\n * process. Neither is large per entry — the leak is in count, not volume — but a cache with no owner\n * for removal is a cache that only grows.\n *\n * Returns the count so the caller can prove the removal; a second discard returns 0, which is what\n * makes the idempotency test possible without exposing the maps.\n */\nexport function discardSession(cwd: string, agentId: string): number {\n const key = transcriptKey(cwd, agentId);\n let removed = 0;\n // `sessions` is NOT erased here — and the distinction is measured, not aesthetic. It holds the\n // readable conversation, and there is a legitimate reader AFTER dispose: the golden\n // `two-concurrent-sends-serialize` calls `getSessionMessages(agentId)` after `agent.dispose()`.\n // Erasing it here returned an empty list and broke two goldens. What bounds it is the LRU ceiling\n // above; the three below are pure bookkeeping, with no post-dispose reader.\n //\n // The key is `transcriptKey(cwd, agentId)` in ALL THREE — not the raw `agentId`. The first version\n // erased two of them by `agentId` and therefore **never erased anything**; the test did not catch\n // it because it only asserted that the SECOND call returns 0, which is true either way.\n if (hydratedKeys.delete(key)) removed++;\n if (pendingWrites.delete(key)) removed++;\n if (recordCounts.delete(key)) removed++;\n return removed;\n}\n\nexport function getSessionMessages(agentId: string): SessionMessage[] {\n return sessions.get(agentId) ?? [];\n}\n\n/**\n * Persist a full conversation turn (user + assistant + tool blocks) to the native\n * transcript. Chained per-(agent, transcript) so on-disk order matches send order,\n * and fire-and-forget so `send()` is not blocked by disk I/O. Every\n * M50 — when the caller supplies `turn.autoCompact`, size-driven auto-compaction (usage real vs\n * the model's context window) runs in this same chain, surfaced via the optional `onCompact` observer.\n *\n * @internal\n */\nexport function persistTurnToTranscript(\n store: SessionStore,\n loc: TranscriptLocation,\n sessionId: string,\n turn: PersistTurnInput,\n onCompact?: () => void,\n): void {\n const key = transcriptKey(loc.cwd, loc.agentId);\n // PRE-EXISTING debt, exposed when M75 fixed the Biome config that used to abort before\n // sweeping these files (a nested root under refactor/). It is not new code and was not touched\n // by M75; refactoring SDK internals without review would trade a visible problem for a diff\n // risky. Tracked in usetheodev/theokit-sdk#151.\n // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: see the reason just above\n const chained = (pendingWrites.get(key) ?? Promise.resolve()).then(async () => {\n try {\n await persistTurn(store, loc, sessionId, turn);\n const count = (recordCounts.get(key) ?? 0) + 1;\n recordCounts.set(key, count);\n // M50 — the 50-turn no-summary boundary stub is GONE (it silently amnesia'd the session).\n // Auto-compaction is now size-driven with a real summary: see `maybeAutoCompact` below,\n // invoked in this same write chain by the post-run lifecycle when usage is known.\n if (turn.autoCompact !== undefined) {\n const { autoCompactIfNeeded } = await import(\"./compact-session.js\");\n const fired = await autoCompactIfNeeded({\n store,\n loc,\n sessionId,\n usageTotal: turn.autoCompact.usageTotal,\n contextWindow: turn.autoCompact.contextWindow,\n turnCount: count,\n summarize: turn.autoCompact.summarize,\n });\n if (fired) onCompact?.();\n }\n } catch (cause) {\n const msg = cause instanceof Error ? cause.message : String(cause);\n diag(`[theokit-sdk] session transcript write failed (${loc.agentId}): ${msg}\\n`);\n }\n });\n pendingWrites.set(\n key,\n chained.then(\n () => undefined,\n () => undefined,\n ),\n );\n}\n\n/**\n * Load the persisted transcript into the in-memory cache. Idempotent per\n * (baseDir, cwd, agentId). Call once per agent lifecycle before the first read.\n *\n * @internal\n */\nexport async function hydrateSession(\n agentId: string,\n loc: { store: SessionStore; cwd: string },\n): Promise<void> {\n const key = transcriptKey(loc.cwd, agentId);\n if (hydratedKeys.has(key)) return;\n hydratedKeys.add(key);\n\n const persisted = await readSessionMessages(loc.store, agentId);\n if (persisted.length === 0) return;\n // M51 review F4 — the DISK is the source of truth at hydration time: after an invalidation\n // (compact/inject), an in-flight turn may have repopulated the cache with a SINGLE message before\n // this hydrate ran; the old \"skip when non-empty\" guard then pinned the parent to a 1-message\n // context (history + injected pair lost until restart). The persist chain serializes writes, so\n // the disk already contains that in-flight turn — replacing is always correct.\n sessions.set(agentId, persisted);\n}\n\n/**\n * Wait for all pending transcript writes to settle. Used by tests and by the\n * agent dispose path so on-disk state matches in-memory before the caller proceeds.\n *\n * @internal\n */\nexport async function flushSessionWrites(): Promise<void> {\n while (pendingWrites.size > 0) {\n const all = Array.from(pendingWrites.values());\n pendingWrites.clear();\n await Promise.all(all);\n }\n}\n\n/**\n * Drop the in-memory message cache for `agentId` and NOTHING else — the hydration marker in\n * `hydratedKeys` deliberately stays.\n *\n * That asymmetry is the feature, not an oversight, and it is what separates this from\n * `invalidateSessionCache`, which drops both. The single caller is the personality switch's\n * `reset: true` path (`internal/personality/switch.ts`): leaving the marker set makes\n * `hydrateSession` return early, so the next send starts from an EMPTY context instead of\n * replaying the pre-reset transcript off disk. Clearing the marker here would re-hydrate the\n * conversation the reset was asked to discard.\n *\n * The same shape read as a bug one function up: `forgetBookkeeping` documents an evicted session\n * coming back empty as \"silent amnesia\". The difference is intent — eviction wants the transcript\n * back, a reset does not. Anyone reaching for this to invalidate a cache wants\n * `invalidateSessionCache(cwd, agentId)` instead.\n */\nexport function clearSession(agentId: string): void {\n sessions.delete(agentId);\n}\n\n/**\n * M50 review F5 — run `fn` serialized on the SAME per-(cwd,agentId) write chain the per-turn\n * persistence uses, so a manual `Agent.compact` can never interleave with an in-flight turn's\n * writes (boundary landing mid-turn would orphan the turn from the replay).\n */\nexport function enqueueSessionWrite<T>(\n cwd: string,\n agentId: string,\n fn: () => Promise<T>,\n): Promise<T> {\n const key = transcriptKey(cwd, agentId);\n const prior = pendingWrites.get(key) ?? Promise.resolve();\n const result = prior.then(fn);\n pendingWrites.set(\n key,\n result.then(\n () => undefined,\n () => undefined,\n ),\n );\n return result;\n}\n\n/** Test-only: drop every cached session and hydration marker. @internal */\nexport function clearAllSessions(): void {\n sessions.clear();\n hydratedKeys.clear();\n recordCounts.clear();\n // M50 review F11 — the auto-compact attempt marks live on globalThis; tests reset them here.\n const g = globalThis as unknown as Record<symbol, Map<string, number>>;\n const sym = Symbol.for(\"theokit-sdk.session.auto-compact-attempts\");\n g[sym]?.clear();\n}\n"]}
1
+ {"version":3,"sources":["../src/internal/session/agent-session.ts"],"names":[],"mappings":"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","file":"chunk-WG7R5W6R.js","sourcesContent":["import type { SessionStore } from \"../../types/session-store.js\";\nimport {\n type PersistTurnInput,\n persistTurn,\n readSessionMessages,\n type TranscriptLocation,\n} from \"./agent-session-store.js\";\n\n/**\n * Per-agent conversation history kept across runs (and across `Agent.resume()`\n * within the same process). Lets the fixture responder recall prior facts when\n * the user asks a follow-up question.\n *\n * SE40 — disk persistence IS the native Claude-shaped `.jsonl` transcript. The\n * in-memory cache holds the narrowed `SessionMessage[]` (user/assistant text) so\n * a send can read `priorMessages` synchronously; the whole rich turn (user +\n * assistant + tool blocks) lands on disk once per send via {@link persistTurnToTranscript}\n * (fed by `run.conversation()`). Hydration on resume reconstructs the session by\n * walking the transcript DAG (`readSessionMessages`).\n *\n * @internal\n */\n\n// `SessionMessage` lives in `./session-types.ts` (leaf types file). Re-exported\n// for back-compat with downstream importers that pulled it from here.\nexport type { SessionMessage } from \"./session-types.js\";\n\nimport type { SessionMessage } from \"./session-types.js\";\n\n// M75 — the cache lives in a leaf; see session-cache.ts for the reason (cycle broken by extraction).\nexport {\n hydratedKeys,\n invalidateSessionCache,\n sessions,\n transcriptKey,\n} from \"./session-cache.js\";\n\nimport { diag } from \"../diagnostics.js\";\nimport { hydratedKeys, sessions, transcriptKey } from \"./session-cache.js\";\n\nconst pendingWrites = new Map<string, Promise<void>>();\nconst recordCounts = new Map<string, number>();\n\n/**\n * Append a session message to the in-memory cache only. Disk persistence for the\n * whole turn happens once per send via {@link persistTurnToTranscript}; the cache\n * feeds `priorMessages` / `onBeforeSend.previousMessageCount` synchronously.\n *\n * @internal\n */\nexport function appendSessionMessage(agentId: string, message: SessionMessage): void {\n const existing = sessions.get(agentId) ?? [];\n existing.push(message);\n // `delete` + `set` reinserts at the END: a JS `Map` preserves insertion order, so the first key\n // is always the least recently touched. That is the entire LRU, with no new structure (parsimony\n // rungs 2/5 — the ordering we need is already a language guarantee).\n sessions.delete(agentId);\n sessions.set(agentId, existing);\n enforceCeiling();\n}\n\n/**\n * Ceiling on sessions kept in memory.\n *\n * The runtime only reads the **active** session; the rest is pure cache, rebuildable from the\n * on-disk transcript. 32 is deliberately generous — the primary removal path is the explicit\n * `discardSession()` at the end of the agent's life, and this ceiling is a safety net against a\n * long-lived process running hundreds of sessions (plan risk #2: a tight ceiling could evict a\n * session still referenced by an in-flight async path).\n */\nexport const MAX_CACHED_SESSIONS = 32;\n\nfunction enforceCeiling(): void {\n while (sessions.size > MAX_CACHED_SESSIONS) {\n const oldest = sessions.keys().next().value;\n if (oldest === undefined) return;\n sessions.delete(oldest);\n forgetBookkeeping(oldest);\n }\n}\n\n/**\n * Erases the bookkeeping for an `agentId` across EVERY `cwd` it appears in.\n *\n * The three maps are keyed by `transcriptKey(cwd, agentId)`; `sessions` is the only one keyed by the\n * raw `agentId`. The ceiling's first version deleted from all three using `sessions`' key — that is,\n * deleted nothing — and left `hydratedKeys` **orphaned**. Since `hydrateSession` returns early when\n * the marker is present, an evicted session came back **empty** instead of rehydrating from disk:\n * silent amnesia, and a regression new to M95, because before it nothing evicted.\n *\n * The ceiling only knows the `agentId`, not the `cwd`, so it scans by suffix — which is the format\n * `transcriptKey` produces. A scan rather than an index because these maps hold tens of entries, not\n * thousands: a reverse index here would be new structure for a problem that does not exist.\n */\nfunction forgetBookkeeping(agentId: string): void {\n const suffix = transcriptKey(\"\", agentId).slice(0 - agentId.length - 2);\n for (const k of [...hydratedKeys]) if (k.endsWith(suffix)) hydratedKeys.delete(k);\n for (const k of [...pendingWrites.keys()]) if (k.endsWith(suffix)) pendingWrites.delete(k);\n for (const k of [...recordCounts.keys()]) if (k.endsWith(suffix)) recordCounts.delete(k);\n}\n\n/**\n * Erases the agent's module bookkeeping and returns how many entries were removed.\n *\n * M95 — `invalidateSessionCache` used to clear **two** of the four maps (`sessions`, `hydratedKeys`);\n * `pendingWrites` and `recordCounts` were never touched by id, so they grew for the life of the\n * process. Neither is large per entry — the leak is in count, not volume — but a cache with no owner\n * for removal is a cache that only grows.\n *\n * Returns the count so the caller can prove the removal; a second discard returns 0, which is what\n * makes the idempotency test possible without exposing the maps.\n */\nexport function discardSession(cwd: string, agentId: string): number {\n const key = transcriptKey(cwd, agentId);\n let removed = 0;\n // `sessions` is NOT erased here — and the distinction is measured, not aesthetic. It holds the\n // readable conversation, and there is a legitimate reader AFTER dispose: the golden\n // `two-concurrent-sends-serialize` calls `getSessionMessages(agentId)` after `agent.dispose()`.\n // Erasing it here returned an empty list and broke two goldens. What bounds it is the LRU ceiling\n // above; the three below are pure bookkeeping, with no post-dispose reader.\n //\n // The key is `transcriptKey(cwd, agentId)` in ALL THREE — not the raw `agentId`. The first version\n // erased two of them by `agentId` and therefore **never erased anything**; the test did not catch\n // it because it only asserted that the SECOND call returns 0, which is true either way.\n if (hydratedKeys.delete(key)) removed++;\n if (pendingWrites.delete(key)) removed++;\n if (recordCounts.delete(key)) removed++;\n return removed;\n}\n\nexport function getSessionMessages(agentId: string): SessionMessage[] {\n return sessions.get(agentId) ?? [];\n}\n\n/**\n * Persist a full conversation turn (user + assistant + tool blocks) to the native\n * transcript. Chained per-(agent, transcript) so on-disk order matches send order,\n * and fire-and-forget so `send()` is not blocked by disk I/O. Every\n * M50 — when the caller supplies `turn.autoCompact`, size-driven auto-compaction (usage real vs\n * the model's context window) runs in this same chain, surfaced via the optional `onCompact` observer.\n *\n * @internal\n */\nexport function persistTurnToTranscript(\n store: SessionStore,\n loc: TranscriptLocation,\n sessionId: string,\n turn: PersistTurnInput,\n onCompact?: () => void,\n): void {\n const key = transcriptKey(loc.cwd, loc.agentId);\n // PRE-EXISTING debt, exposed when M75 fixed the Biome config that used to abort before\n // sweeping these files (a nested root under refactor/). It is not new code and was not touched\n // by M75; refactoring SDK internals without review would trade a visible problem for a diff\n // risky. Tracked in usetheodev/theokit-sdk#151.\n // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: see the reason just above\n const chained = (pendingWrites.get(key) ?? Promise.resolve()).then(async () => {\n try {\n await persistTurn(store, loc, sessionId, turn);\n const count = (recordCounts.get(key) ?? 0) + 1;\n recordCounts.set(key, count);\n // M50 — the 50-turn no-summary boundary stub is GONE (it silently amnesia'd the session).\n // Auto-compaction is now size-driven with a real summary: see `maybeAutoCompact` below,\n // invoked in this same write chain by the post-run lifecycle when usage is known.\n if (turn.autoCompact !== undefined) {\n const { autoCompactIfNeeded } = await import(\"./compact-session.js\");\n const fired = await autoCompactIfNeeded({\n store,\n loc,\n sessionId,\n usageTotal: turn.autoCompact.usageTotal,\n contextWindow: turn.autoCompact.contextWindow,\n turnCount: count,\n summarize: turn.autoCompact.summarize,\n });\n if (fired) onCompact?.();\n }\n } catch (cause) {\n const msg = cause instanceof Error ? cause.message : String(cause);\n diag(`[theokit-sdk] session transcript write failed (${loc.agentId}): ${msg}\\n`);\n }\n });\n pendingWrites.set(\n key,\n chained.then(\n () => undefined,\n () => undefined,\n ),\n );\n}\n\n/**\n * Load the persisted transcript into the in-memory cache. Idempotent per\n * (baseDir, cwd, agentId). Call once per agent lifecycle before the first read.\n *\n * @internal\n */\nexport async function hydrateSession(\n agentId: string,\n loc: { store: SessionStore; cwd: string },\n): Promise<void> {\n const key = transcriptKey(loc.cwd, agentId);\n if (hydratedKeys.has(key)) return;\n hydratedKeys.add(key);\n\n const persisted = await readSessionMessages(loc.store, agentId);\n if (persisted.length === 0) return;\n // M51 review F4 — the DISK is the source of truth at hydration time: after an invalidation\n // (compact/inject), an in-flight turn may have repopulated the cache with a SINGLE message before\n // this hydrate ran; the old \"skip when non-empty\" guard then pinned the parent to a 1-message\n // context (history + injected pair lost until restart). The persist chain serializes writes, so\n // the disk already contains that in-flight turn — replacing is always correct.\n sessions.set(agentId, persisted);\n}\n\n/**\n * Wait for all pending transcript writes to settle. Used by tests and by the\n * agent dispose path so on-disk state matches in-memory before the caller proceeds.\n *\n * @internal\n */\nexport async function flushSessionWrites(): Promise<void> {\n while (pendingWrites.size > 0) {\n const all = Array.from(pendingWrites.values());\n pendingWrites.clear();\n await Promise.all(all);\n }\n}\n\n/**\n * Drop the in-memory message cache for `agentId` and NOTHING else — the hydration marker in\n * `hydratedKeys` deliberately stays.\n *\n * That asymmetry is the feature, not an oversight, and it is what separates this from\n * `invalidateSessionCache`, which drops both. The single caller is the personality switch's\n * `reset: true` path (`internal/personality/switch.ts`): leaving the marker set makes\n * `hydrateSession` return early, so the next send starts from an EMPTY context instead of\n * replaying the pre-reset transcript off disk. Clearing the marker here would re-hydrate the\n * conversation the reset was asked to discard.\n *\n * The same shape read as a bug one function up: `forgetBookkeeping` documents an evicted session\n * coming back empty as \"silent amnesia\". The difference is intent — eviction wants the transcript\n * back, a reset does not. Anyone reaching for this to invalidate a cache wants\n * `invalidateSessionCache(cwd, agentId)` instead.\n */\nexport function clearSession(agentId: string): void {\n sessions.delete(agentId);\n}\n\n/**\n * M50 review F5 — run `fn` serialized on the SAME per-(cwd,agentId) write chain the per-turn\n * persistence uses, so a manual `Agent.compact` can never interleave with an in-flight turn's\n * writes (boundary landing mid-turn would orphan the turn from the replay).\n */\nexport function enqueueSessionWrite<T>(\n cwd: string,\n agentId: string,\n fn: () => Promise<T>,\n): Promise<T> {\n const key = transcriptKey(cwd, agentId);\n const prior = pendingWrites.get(key) ?? Promise.resolve();\n const result = prior.then(fn);\n pendingWrites.set(\n key,\n result.then(\n () => undefined,\n () => undefined,\n ),\n );\n return result;\n}\n\n/** Test-only: drop every cached session and hydration marker. @internal */\nexport function clearAllSessions(): void {\n sessions.clear();\n hydratedKeys.clear();\n recordCounts.clear();\n // M50 review F11 — the auto-compact attempt marks live on globalThis; tests reset them here.\n const g = globalThis as unknown as Record<symbol, Map<string, number>>;\n const sym = Symbol.for(\"theokit-sdk.session.auto-compact-attempts\");\n g[sym]?.clear();\n}\n"]}