supipowers 1.5.3 → 2.0.1

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 (340) hide show
  1. package/README.md +14 -8
  2. package/bin/install.mjs +20 -5
  3. package/bin/install.ts +95 -0
  4. package/package.json +8 -4
  5. package/skills/context-mode/SKILL.md +17 -10
  6. package/skills/harness/SKILL.md +94 -0
  7. package/skills/ui-design/SKILL.md +63 -0
  8. package/skills/ui-design/sub-agent-templates/component-builder.md +29 -0
  9. package/skills/ui-design/sub-agent-templates/design-critic.md +46 -0
  10. package/skills/ui-design/sub-agent-templates/pencil/component-builder.md +29 -0
  11. package/skills/ui-design/sub-agent-templates/pencil/design-critic.md +42 -0
  12. package/skills/ui-design/sub-agent-templates/pencil/section-assembler.md +27 -0
  13. package/skills/ui-design/sub-agent-templates/section-assembler.md +27 -0
  14. package/skills/ultraplan-discover/SKILL.md +96 -0
  15. package/skills/ultraplan-intake/SKILL.md +89 -0
  16. package/skills/ultraplan-research/SKILL.md +129 -0
  17. package/skills/ultraplan-review/SKILL.md +86 -0
  18. package/skills/ultraplan-review-scope/SKILL.md +111 -0
  19. package/skills/ultraplan-review-structure/SKILL.md +120 -0
  20. package/skills/ultraplan-review-tdd/SKILL.md +142 -0
  21. package/skills/ultraplan-scout/SKILL.md +110 -0
  22. package/skills/ultraplan-synthesize/SKILL.md +124 -0
  23. package/src/{quality/ai-session.ts → ai/final-message.ts} +27 -0
  24. package/src/ai/schema-text.ts +129 -0
  25. package/src/ai/structured-output.ts +274 -0
  26. package/src/ai/template.ts +27 -0
  27. package/src/bootstrap.ts +63 -28
  28. package/src/commands/agents.ts +131 -42
  29. package/src/commands/ai-review.ts +251 -30
  30. package/src/commands/clear.ts +434 -0
  31. package/src/commands/commit.ts +1 -0
  32. package/src/commands/config.ts +242 -44
  33. package/src/commands/context.ts +55 -28
  34. package/src/commands/doctor.ts +234 -6
  35. package/src/commands/fix-pr.ts +306 -131
  36. package/src/commands/generate.ts +111 -21
  37. package/src/commands/memory.ts +192 -0
  38. package/src/commands/model-picker.ts +28 -21
  39. package/src/commands/model.ts +18 -8
  40. package/src/commands/optimize-context.ts +408 -29
  41. package/src/commands/plan.ts +2 -0
  42. package/src/commands/qa.ts +312 -137
  43. package/src/commands/release.ts +259 -76
  44. package/src/commands/review.ts +293 -59
  45. package/src/commands/status.ts +200 -13
  46. package/src/commands/supi.ts +3 -35
  47. package/src/commands/ui-design.ts +394 -0
  48. package/src/commands/ultraplan.ts +1518 -0
  49. package/src/commands/update.ts +86 -0
  50. package/src/config/defaults.ts +62 -0
  51. package/src/config/loader.ts +448 -60
  52. package/src/config/schema.ts +108 -2
  53. package/src/context/optimizer.ts +25 -33
  54. package/src/context/rule-renderer.ts +223 -0
  55. package/src/context/savings.ts +258 -0
  56. package/src/context/startup-check.ts +380 -0
  57. package/src/context/startup-optimizer.ts +355 -0
  58. package/src/context/tokenignore.ts +146 -0
  59. package/src/context-mode/cache-handle.ts +49 -0
  60. package/src/context-mode/cache-preview.ts +71 -0
  61. package/src/context-mode/cache-store.ts +738 -0
  62. package/src/context-mode/compressor.ts +131 -26
  63. package/src/context-mode/dedup.ts +108 -0
  64. package/src/context-mode/detector.ts +35 -4
  65. package/src/context-mode/event-extractor.ts +14 -12
  66. package/src/context-mode/event-store.ts +91 -36
  67. package/src/context-mode/hooks.ts +798 -56
  68. package/src/context-mode/knowledge/store.ts +255 -11
  69. package/src/context-mode/memory-store.ts +325 -0
  70. package/src/context-mode/metrics-recorder.ts +158 -0
  71. package/src/context-mode/metrics-store.ts +765 -0
  72. package/src/context-mode/model.ts +24 -0
  73. package/src/context-mode/processor-keys.ts +29 -0
  74. package/src/context-mode/processors/build.ts +66 -0
  75. package/src/context-mode/processors/docker.ts +57 -0
  76. package/src/context-mode/processors/git.ts +111 -0
  77. package/src/context-mode/processors/json.ts +112 -0
  78. package/src/context-mode/processors/k8s.ts +67 -0
  79. package/src/context-mode/processors/lint.ts +67 -0
  80. package/src/context-mode/processors/log.ts +86 -0
  81. package/src/context-mode/processors/registry.ts +116 -0
  82. package/src/context-mode/processors/test-runner.ts +102 -0
  83. package/src/context-mode/processors/types.ts +20 -0
  84. package/src/context-mode/repomap.ts +400 -0
  85. package/src/context-mode/routing.ts +97 -24
  86. package/src/context-mode/sandbox/runners.ts +5 -1
  87. package/src/context-mode/snapshot-builder.ts +106 -11
  88. package/src/context-mode/source-hash.ts +173 -0
  89. package/src/context-mode/tool-name.ts +11 -0
  90. package/src/context-mode/tools.ts +654 -22
  91. package/src/context-mode/web/fetcher.ts +31 -12
  92. package/src/debug/logger.ts +2 -1
  93. package/src/deps/registry.ts +1 -1
  94. package/src/discipline/failure-summarizer.ts +170 -0
  95. package/src/discipline/failure-taxonomy.ts +131 -0
  96. package/src/discipline/workflow-invariants.ts +125 -0
  97. package/src/discovery/index.ts +31 -0
  98. package/src/discovery/lsp.ts +87 -0
  99. package/src/discovery/rank.ts +144 -0
  100. package/src/discovery/sources.ts +89 -0
  101. package/src/discovery/workflow.ts +87 -0
  102. package/src/docs/contracts.ts +39 -0
  103. package/src/docs/drift.ts +117 -87
  104. package/src/fix-pr/assessment.ts +200 -0
  105. package/src/fix-pr/contracts.ts +47 -0
  106. package/src/fix-pr/fetch-comments.ts +80 -0
  107. package/src/fix-pr/prompt-builder.ts +58 -40
  108. package/src/fix-pr/scripts/exec.ts +34 -0
  109. package/src/fix-pr/scripts/trigger-review.ts +106 -0
  110. package/src/fix-pr/scripts/wait-and-check.ts +108 -0
  111. package/src/fix-pr/types.ts +4 -0
  112. package/src/git/branch-finish.ts +5 -0
  113. package/src/git/commit-contract.ts +83 -0
  114. package/src/git/commit.ts +121 -184
  115. package/src/git/status.ts +62 -8
  116. package/src/harness/anti_slop/architecture-parser.ts +210 -0
  117. package/src/harness/anti_slop/backend-factory.ts +30 -0
  118. package/src/harness/anti_slop/backend.ts +140 -0
  119. package/src/harness/anti_slop/desloppify-adapter.ts +319 -0
  120. package/src/harness/anti_slop/fallow-adapter.ts +305 -0
  121. package/src/harness/anti_slop/installer.ts +227 -0
  122. package/src/harness/anti_slop/queue.ts +216 -0
  123. package/src/harness/anti_slop/recommend.ts +84 -0
  124. package/src/harness/anti_slop/score.ts +180 -0
  125. package/src/harness/anti_slop/synthetic-edit-test.ts +128 -0
  126. package/src/harness/artifacts/agents-md.ts +88 -0
  127. package/src/harness/artifacts/checks-wiring.ts +57 -0
  128. package/src/harness/artifacts/docs-tree.ts +79 -0
  129. package/src/harness/artifacts/lint-configs.ts +136 -0
  130. package/src/harness/artifacts/review-agents.ts +67 -0
  131. package/src/harness/bare-entry.ts +108 -0
  132. package/src/harness/command.ts +1010 -0
  133. package/src/harness/default-agents/design.md +23 -0
  134. package/src/harness/default-agents/discover.md +18 -0
  135. package/src/harness/default-agents/implement.md +24 -0
  136. package/src/harness/default-agents/plan.md +19 -0
  137. package/src/harness/default-agents/research.md +21 -0
  138. package/src/harness/default-agents/validate.md +22 -0
  139. package/src/harness/gc/reporter.ts +28 -0
  140. package/src/harness/gc/runner.ts +136 -0
  141. package/src/harness/hooks/layer-context-inject.ts +155 -0
  142. package/src/harness/hooks/post-session-sweep.ts +130 -0
  143. package/src/harness/hooks/pre-edit-dupe-probe.ts +224 -0
  144. package/src/harness/hooks/register.ts +118 -0
  145. package/src/harness/model.ts +117 -0
  146. package/src/harness/pipeline.ts +348 -0
  147. package/src/harness/project-paths.ts +235 -0
  148. package/src/harness/stage-runner.ts +107 -0
  149. package/src/harness/stages/design.ts +386 -0
  150. package/src/harness/stages/discover.ts +454 -0
  151. package/src/harness/stages/implement.ts +162 -0
  152. package/src/harness/stages/plan.ts +335 -0
  153. package/src/harness/stages/research.ts +263 -0
  154. package/src/harness/stages/validate.ts +684 -0
  155. package/src/harness/storage.ts +467 -0
  156. package/src/harness/tools.ts +426 -0
  157. package/src/lsp/bridge.ts +56 -95
  158. package/src/lsp/capabilities.ts +108 -0
  159. package/src/lsp/contracts.ts +35 -0
  160. package/src/lsp/detector.ts +8 -12
  161. package/src/markdown-frontmatter.ts +68 -0
  162. package/src/mempalace/bridge.ts +135 -0
  163. package/src/mempalace/config.ts +75 -0
  164. package/src/mempalace/format.ts +163 -0
  165. package/src/mempalace/hooks.ts +370 -0
  166. package/src/mempalace/installer-helper.ts +194 -0
  167. package/src/mempalace/python/mempalace_bridge.py +440 -0
  168. package/src/mempalace/runtime.ts +565 -0
  169. package/src/mempalace/schema.ts +268 -0
  170. package/src/mempalace/session-summary.ts +198 -0
  171. package/src/mempalace/tool.ts +186 -0
  172. package/src/mempalace/uv.ts +256 -0
  173. package/src/migrate/runner.ts +354 -0
  174. package/src/planning/approval-flow.ts +206 -9
  175. package/src/planning/plan-writer-prompt.ts +4 -3
  176. package/src/planning/planning-ask-tool.ts +39 -0
  177. package/src/planning/render-markdown.ts +74 -0
  178. package/src/planning/spec.ts +42 -0
  179. package/src/planning/system-prompt.ts +11 -8
  180. package/src/planning/validate.ts +84 -0
  181. package/src/platform/omp.ts +15 -2
  182. package/src/platform/system-prompt.ts +37 -0
  183. package/src/platform/test-utils.ts +3 -0
  184. package/src/platform/types.ts +6 -1
  185. package/src/qa/config.ts +12 -6
  186. package/src/qa/detect-app-type.ts +13 -6
  187. package/src/qa/matrix.ts +12 -6
  188. package/src/qa/prompt-builder.ts +28 -30
  189. package/src/qa/scripts/dev-server-utils.ts +72 -0
  190. package/src/qa/scripts/run-e2e-tests.ts +226 -0
  191. package/src/qa/scripts/start-dev-server.ts +138 -0
  192. package/src/qa/scripts/stop-dev-server.ts +77 -0
  193. package/src/qa/session.ts +13 -7
  194. package/src/quality/ai-setup.ts +27 -25
  195. package/src/quality/contracts.ts +34 -0
  196. package/src/quality/gates/ai-review.ts +20 -58
  197. package/src/quality/gates/command.ts +249 -46
  198. package/src/quality/review-gates.ts +18 -2
  199. package/src/quality/runner.ts +63 -22
  200. package/src/quality/schemas.ts +37 -2
  201. package/src/quality/setup.ts +96 -16
  202. package/src/release/changelog.ts +1 -1
  203. package/src/release/channels/custom.ts +13 -3
  204. package/src/release/channels/types.ts +5 -0
  205. package/src/release/contracts.ts +90 -0
  206. package/src/release/executor.ts +122 -45
  207. package/src/release/prompt.ts +18 -2
  208. package/src/release/targets.ts +86 -0
  209. package/src/release/version.ts +96 -71
  210. package/src/review/agent-loader.ts +221 -109
  211. package/src/review/fixer.ts +10 -6
  212. package/src/review/multi-agent-runner.ts +114 -13
  213. package/src/review/output.ts +12 -139
  214. package/src/review/runner.ts +12 -6
  215. package/src/review/scope.ts +144 -24
  216. package/src/review/types.ts +1 -20
  217. package/src/review/validator.ts +12 -6
  218. package/src/storage/fix-pr-sessions.ts +21 -14
  219. package/src/storage/plans.ts +14 -5
  220. package/src/storage/qa-sessions.ts +25 -19
  221. package/src/storage/reliability-metrics.ts +180 -0
  222. package/src/storage/reports.ts +8 -7
  223. package/src/storage/review-sessions.ts +55 -20
  224. package/src/tool-catalog/active-tool-controller.ts +164 -0
  225. package/src/tool-catalog/active-tool-planner.ts +212 -0
  226. package/src/tool-catalog/tool-groups.ts +102 -0
  227. package/src/types.ts +1399 -5
  228. package/src/ui-design/backend-adapter.ts +78 -0
  229. package/src/ui-design/backends/local-html.ts +82 -0
  230. package/src/ui-design/backends/pencil-mcp.ts +111 -0
  231. package/src/ui-design/components-scanner.ts +124 -0
  232. package/src/ui-design/config.ts +55 -0
  233. package/src/ui-design/pen-scanner.ts +95 -0
  234. package/src/ui-design/pen-selector.ts +72 -0
  235. package/src/ui-design/prompt-builder.ts +73 -0
  236. package/src/ui-design/scanner.ts +136 -0
  237. package/src/ui-design/session.ts +974 -0
  238. package/src/ui-design/system-prompt.ts +312 -0
  239. package/src/ui-design/tokens-scanner.ts +181 -0
  240. package/src/ui-design/types.ts +96 -0
  241. package/src/ultraplan/agent-catalog.ts +522 -0
  242. package/src/ultraplan/authoring/agent-catalog.ts +310 -0
  243. package/src/ultraplan/authoring/authoring-tools.ts +552 -0
  244. package/src/ultraplan/authoring/command-handlers.ts +339 -0
  245. package/src/ultraplan/authoring/markdown.ts +510 -0
  246. package/src/ultraplan/authoring/model.ts +162 -0
  247. package/src/ultraplan/authoring/pipeline.ts +319 -0
  248. package/src/ultraplan/authoring/stage-runner.ts +141 -0
  249. package/src/ultraplan/authoring/stages/approve.ts +249 -0
  250. package/src/ultraplan/authoring/stages/discover.ts +289 -0
  251. package/src/ultraplan/authoring/stages/intake.ts +203 -0
  252. package/src/ultraplan/authoring/stages/research.ts +399 -0
  253. package/src/ultraplan/authoring/stages/review.ts +333 -0
  254. package/src/ultraplan/authoring/stages/scout.ts +188 -0
  255. package/src/ultraplan/authoring/stages/synthesize.ts +348 -0
  256. package/src/ultraplan/authoring/storage.ts +594 -0
  257. package/src/ultraplan/authoring/synth-gate.ts +165 -0
  258. package/src/ultraplan/authoring-draft.ts +653 -0
  259. package/src/ultraplan/authoring-persist.ts +180 -0
  260. package/src/ultraplan/authoring-tool.ts +608 -0
  261. package/src/ultraplan/authoring-wizard.ts +587 -0
  262. package/src/ultraplan/batch/merge.ts +98 -0
  263. package/src/ultraplan/batch/planner.ts +150 -0
  264. package/src/ultraplan/batch/presenter.ts +97 -0
  265. package/src/ultraplan/batch/storage.ts +420 -0
  266. package/src/ultraplan/batch/supervisor.ts +317 -0
  267. package/src/ultraplan/batch/worker.ts +26 -0
  268. package/src/ultraplan/batch/worktree.ts +110 -0
  269. package/src/ultraplan/contracts.ts +1593 -0
  270. package/src/ultraplan/default-agents/authoring/discoverer.md +12 -0
  271. package/src/ultraplan/default-agents/authoring/intake.md +12 -0
  272. package/src/ultraplan/default-agents/authoring/planner.md +12 -0
  273. package/src/ultraplan/default-agents/authoring/researcher.md +12 -0
  274. package/src/ultraplan/default-agents/authoring/scope-checker.md +12 -0
  275. package/src/ultraplan/default-agents/authoring/scout.md +12 -0
  276. package/src/ultraplan/default-agents/authoring/structure-checker.md +12 -0
  277. package/src/ultraplan/default-agents/authoring/tdd-checker.md +12 -0
  278. package/src/ultraplan/default-agents/backend-domain-reviewer.md +10 -0
  279. package/src/ultraplan/default-agents/backend-executor.md +10 -0
  280. package/src/ultraplan/default-agents/backend-stack-reviewer.md +10 -0
  281. package/src/ultraplan/default-agents/backend-tester.md +10 -0
  282. package/src/ultraplan/default-agents/frontend-domain-reviewer.md +10 -0
  283. package/src/ultraplan/default-agents/frontend-executor.md +10 -0
  284. package/src/ultraplan/default-agents/frontend-stack-reviewer.md +10 -0
  285. package/src/ultraplan/default-agents/frontend-tester.md +10 -0
  286. package/src/ultraplan/default-agents/infrastructure-domain-reviewer.md +10 -0
  287. package/src/ultraplan/default-agents/infrastructure-executor.md +10 -0
  288. package/src/ultraplan/default-agents/infrastructure-stack-reviewer.md +10 -0
  289. package/src/ultraplan/default-agents/infrastructure-tester.md +10 -0
  290. package/src/ultraplan/execution/contract.ts +71 -0
  291. package/src/ultraplan/execution/policy.ts +217 -0
  292. package/src/ultraplan/execution/runtime-tools.ts +107 -0
  293. package/src/ultraplan/execution/session-runner.ts +281 -0
  294. package/src/ultraplan/next-router.ts +85 -0
  295. package/src/ultraplan/presenter.ts +359 -0
  296. package/src/ultraplan/project-paths.ts +342 -0
  297. package/src/ultraplan/runtime/active-execution.ts +72 -0
  298. package/src/ultraplan/runtime/apply-mutation.ts +416 -0
  299. package/src/ultraplan/runtime/blockers.ts +243 -0
  300. package/src/ultraplan/runtime/hook-bridge.ts +486 -0
  301. package/src/ultraplan/runtime/launch-context.ts +207 -0
  302. package/src/ultraplan/runtime/migration.ts +524 -0
  303. package/src/ultraplan/runtime/normalize.ts +281 -0
  304. package/src/ultraplan/runtime/proof.ts +260 -0
  305. package/src/ultraplan/runtime/reducer.ts +416 -0
  306. package/src/ultraplan/runtime/repair.ts +251 -0
  307. package/src/ultraplan/runtime/tracker-storage.ts +368 -0
  308. package/src/ultraplan/session-selection.ts +291 -0
  309. package/src/ultraplan/storage.ts +374 -0
  310. package/src/utils/editor.ts +38 -0
  311. package/src/utils/executable.ts +80 -0
  312. package/src/utils/paths.ts +1 -20
  313. package/src/utils/shell.ts +31 -0
  314. package/src/visual/companion.ts +2 -1
  315. package/src/visual/scripts/frame-template.html +60 -0
  316. package/src/visual/scripts/index.js +59 -13
  317. package/src/visual/scripts/package.json +3 -0
  318. package/src/visual/start-server.ts +2 -1
  319. package/src/workspace/git-scope.ts +64 -0
  320. package/src/workspace/locks.ts +23 -0
  321. package/src/workspace/package-manager.ts +117 -0
  322. package/src/workspace/path-mapping.ts +75 -0
  323. package/src/workspace/project-slug.ts +92 -0
  324. package/src/workspace/repo-root.ts +137 -0
  325. package/src/workspace/selector.ts +115 -0
  326. package/src/workspace/state-paths.ts +118 -0
  327. package/src/workspace/targets.ts +313 -0
  328. package/src/fix-pr/scripts/diff-comments.sh +0 -33
  329. package/src/fix-pr/scripts/fetch-pr-comments.sh +0 -25
  330. package/src/fix-pr/scripts/trigger-review.sh +0 -36
  331. package/src/fix-pr/scripts/wait-and-check.sh +0 -37
  332. package/src/qa/scripts/detect-app-type.sh +0 -68
  333. package/src/qa/scripts/discover-routes.sh +0 -143
  334. package/src/qa/scripts/run-e2e-tests.sh +0 -131
  335. package/src/qa/scripts/start-dev-server.sh +0 -46
  336. package/src/qa/scripts/stop-dev-server.sh +0 -36
  337. package/src/review/prompts/fix-output-schema.md +0 -18
  338. package/src/review/prompts/review-output-schema.md +0 -38
  339. package/src/review/template.ts +0 -15
  340. /package/src/{review → ai}/prompts/invalid-output-retry.md +0 -0
@@ -1,16 +1,23 @@
1
1
  // src/context-mode/tools.ts
2
2
  //
3
- // Registers 8 native context-mode tools via platform.registerTool().
3
+ // Registers native context-mode tools via platform.registerTool().
4
4
  // Orchestration layer: delegates execution to sandbox, owns intent-driven
5
5
  // filtering (auto-indexing large output into knowledge store).
6
6
 
7
- import { readFileSync } from "node:fs";
7
+ import { readFileSync, readdirSync, statSync } from "node:fs";
8
+ import { extname, isAbsolute, join, resolve } from "node:path";
9
+ import { createHash } from "node:crypto";
8
10
  import type { Platform } from "../platform/types.js";
9
11
  import { executeCode } from "./sandbox/executor.js";
10
12
  import { getSupportedLanguages } from "./sandbox/runners.js";
11
13
  import { chunkMarkdown } from "./knowledge/chunker.js";
12
14
  import { KnowledgeStore } from "./knowledge/store.js";
15
+ import { getCacheStore, getMetricsStore, getSessionId } from "./hooks.js";
13
16
  import { fetchAndIndex } from "./web/fetcher.js";
17
+ import { parseCacheHandle } from "./cache-handle.js";
18
+ import { sliceCachedText } from "./cache-preview.js";
19
+ import { buildRepoMap } from "./repomap.js";
20
+ import { canonicalizeSourcePath } from "./source-hash.js";
14
21
 
15
22
  /** Threshold (bytes) above which intent-driven filtering kicks in. */
16
23
  const INTENT_THRESHOLD = 5 * 1024;
@@ -47,6 +54,123 @@ function trackCall(toolName: string, outputBytes: number): void {
47
54
  stats.bytesReturned += outputBytes;
48
55
  }
49
56
 
57
+ function byteLength(text: string): number {
58
+ return new TextEncoder().encode(text).byteLength;
59
+ }
60
+
61
+ function stripCurrentDirPrefixBeforeWindowsAbsolute(p: string): string {
62
+ return p.replace(/^\.[\\/]+(?=[A-Za-z]:[\\/])/, "");
63
+ }
64
+
65
+ function resolveNativeFilePath(filePath: string, cwd = process.cwd()): string {
66
+ const normalized = stripCurrentDirPrefixBeforeWindowsAbsolute(filePath);
67
+ return isAbsolute(normalized) ? normalized : resolve(cwd, normalized);
68
+ }
69
+
70
+
71
+ function currentKnowledgeOwner() {
72
+ return { ownerScope: "session" as const, ownerId: getSessionId() };
73
+ }
74
+
75
+ function sha256Text(value: string): string {
76
+ return createHash("sha256").update(value).digest("hex");
77
+ }
78
+
79
+ function stableStringify(value: unknown): string {
80
+ if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
81
+ if (value && typeof value === "object") {
82
+ return `{${Object.entries(value as Record<string, unknown>)
83
+ .sort(([a], [b]) => a.localeCompare(b))
84
+ .map(([key, val]) => `${JSON.stringify(key)}:${stableStringify(val)}`)
85
+ .join(",")}}`;
86
+ }
87
+ return JSON.stringify(value);
88
+ }
89
+
90
+ function recordCacheOpenMetric(opts: { beforeBytes: number; afterBytes: number; cacheHit: 0 | 1 }): void {
91
+
92
+ const metrics = getMetricsStore();
93
+ if (!metrics) return;
94
+
95
+ try {
96
+ metrics.record({
97
+ session_id: getSessionId(),
98
+ ts: Date.now(),
99
+ layer: "L3",
100
+ tool: "ctx_open_cached",
101
+ processor: "cache-open",
102
+ before_bytes: Math.max(0, Math.floor(opts.beforeBytes)),
103
+ after_bytes: Math.max(0, Math.floor(opts.afterBytes)),
104
+ cache_hit: opts.cacheHit,
105
+ unique_source_hash: null,
106
+ context_tokens: null,
107
+ context_window: null,
108
+ context_percent: null,
109
+ });
110
+ } catch {
111
+ // Cache reads must not depend on metrics health.
112
+ }
113
+ }
114
+
115
+
116
+ function recordRequestCacheMetric(opts: { tool: string; beforeBytes: number; afterBytes: number; cacheHit: 0 | 1 }): void {
117
+ const metrics = getMetricsStore();
118
+ if (!metrics) return;
119
+ try {
120
+ metrics.record({
121
+ session_id: getSessionId(),
122
+ ts: Date.now(),
123
+ layer: "L3",
124
+ tool: opts.tool,
125
+ processor: "cache-open",
126
+ before_bytes: Math.max(0, Math.floor(opts.beforeBytes)),
127
+ after_bytes: Math.max(0, Math.floor(opts.afterBytes)),
128
+ cache_hit: opts.cacheHit,
129
+ unique_source_hash: null,
130
+ context_tokens: null,
131
+ context_window: null,
132
+ context_percent: null,
133
+ });
134
+ } catch {
135
+ // Request-cache metrics are best-effort.
136
+ }
137
+ }
138
+
139
+ function recordL4RetrievalMetric(tool: "ctx_repomap" | "ctx_symbol", beforeBytes: number, afterBytes: number): void {
140
+ const metrics = getMetricsStore();
141
+ if (!metrics) return;
142
+ try {
143
+ metrics.record({
144
+ session_id: getSessionId(),
145
+ ts: Date.now(),
146
+ layer: "L4",
147
+ tool,
148
+ processor: "passthrough",
149
+ before_bytes: Math.max(0, Math.floor(beforeBytes)),
150
+ after_bytes: Math.max(0, Math.floor(afterBytes)),
151
+ cache_hit: 0,
152
+ unique_source_hash: null,
153
+ context_tokens: null,
154
+ context_window: null,
155
+ context_percent: null,
156
+ });
157
+ } catch {
158
+ // Retrieval metrics are diagnostic-only and must never break tool responses.
159
+ }
160
+ }
161
+
162
+ type KnowledgeStoreProvider = KnowledgeStore | (() => KnowledgeStore | null);
163
+
164
+ function resolveKnowledgeStore(provider: KnowledgeStoreProvider): KnowledgeStore | null {
165
+ return typeof provider === "function" ? provider() : provider;
166
+ }
167
+
168
+ function requireKnowledgeStore(provider: KnowledgeStoreProvider): KnowledgeStore {
169
+ const store = resolveKnowledgeStore(provider);
170
+ if (!store) throw new Error("Knowledge store unavailable for the active session.");
171
+ return store;
172
+ }
173
+
50
174
  /**
51
175
  * If output exceeds INTENT_THRESHOLD and an intent is provided, auto-index
52
176
  * the output and return search results instead of raw text.
@@ -55,15 +179,16 @@ function maybeFilterByIntent(
55
179
  output: string,
56
180
  intent: string | undefined,
57
181
  source: string,
58
- store: KnowledgeStore,
182
+ store: KnowledgeStore | null,
59
183
  ): string {
184
+ if (!store) return output;
60
185
  if (!intent || output.length < INTENT_THRESHOLD) return output;
61
186
 
62
187
  const chunks = chunkMarkdown(output, source);
63
188
  if (chunks.length === 0) return output;
64
189
 
65
- store.index(chunks, source);
66
- const results = store.search([intent], { source, limit: 5 });
190
+ store.index(chunks, source, currentKnowledgeOwner());
191
+ const results = store.search([intent], { source, limit: 5, owner: currentKnowledgeOwner() });
67
192
 
68
193
  const sections = chunks.map((c) => `- ${c.title || "(untitled)"} (${c.body.length}B)`).join("\n");
69
194
 
@@ -150,10 +275,169 @@ function formatSearchResults(grouped: ReturnType<KnowledgeStore["search"]>): str
150
275
  return text;
151
276
  }
152
277
 
153
- export function registerContextModeTools(platform: Platform, store: KnowledgeStore): void {
278
+ // ── Auto-index bootstrap for ctx_search ────────────────────────────
279
+
280
+ /** Extensions worth scanning when bootstrapping the knowledge store. */
281
+ const AUTO_INDEX_EXTENSIONS = new Set([
282
+ ".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs",
283
+ ".py", ".rb", ".go", ".rs", ".java", ".kt", ".swift",
284
+ ".md", ".mdx", ".txt", ".rst",
285
+ ".yaml", ".yml", ".toml", ".json",
286
+ ".sh", ".bash", ".zsh",
287
+ ".html", ".css", ".scss",
288
+ ".sql",
289
+ ]);
290
+
291
+ /** Directory names to skip during bootstrap scan. */
292
+ const AUTO_INDEX_SKIP_DIRS = new Set([
293
+ "node_modules", ".git", ".svn", ".hg",
294
+ "dist", "build", "out", ".next", ".nuxt", ".cache", ".turbo", ".bun",
295
+ "coverage", "vendor", "target", "__pycache__", ".venv", "venv",
296
+ ".pytest_cache", ".mypy_cache", ".ruff_cache", ".tox",
297
+ "tmp", ".tmp", ".idea", ".vscode",
298
+ ]);
299
+
300
+ const AUTO_INDEX_MAX_FILES = 80;
301
+ const AUTO_INDEX_MAX_SCAN = 4000;
302
+ const AUTO_INDEX_MAX_FILE_BYTES = 256 * 1024;
303
+ const AUTO_INDEX_MAX_DEPTH = 8;
304
+ const AUTO_INDEX_MIN_TERM_LEN = 3;
305
+
306
+ /** Sessions for which we have already attempted bootstrap. Prevents repeat scans. */
307
+ const _autoIndexAttempted = new Set<string>();
308
+
309
+ /** Reset auto-index attempt tracking. Test-only. */
310
+ export function _resetAutoIndexAttempts(): void {
311
+ _autoIndexAttempted.clear();
312
+ }
313
+
314
+ /** Drop bootstrap-attempted entries that belong to a closed session. */
315
+ export function _forgetAutoIndexSession(ownerId: string): void {
316
+ if (!ownerId) return;
317
+ const prefix = `${ownerId}|`;
318
+ for (const key of _autoIndexAttempted) {
319
+ if (key.startsWith(prefix)) _autoIndexAttempted.delete(key);
320
+ }
321
+ }
322
+
323
+ function extractIndexTerms(queries: string[]): string[] {
324
+ const terms = new Set<string>();
325
+ for (const query of queries) {
326
+ if (typeof query !== "string") continue;
327
+ const tokens = query.toLowerCase().split(/[^\p{L}\p{N}_]+/u);
328
+ for (const token of tokens) {
329
+ if (token.length >= AUTO_INDEX_MIN_TERM_LEN) terms.add(token);
330
+ }
331
+ }
332
+ return [...terms];
333
+ }
334
+
335
+ /**
336
+ * Bootstrap the knowledge store by scanning `cwd` for files containing any of
337
+ * the query terms, then indexing those files. Used when ctx_search is called
338
+ * against an empty store — without this, search can never return useful
339
+ * results until the agent manually runs ctx_index/ctx_batch_execute.
340
+ *
341
+ * Returns the number of chunks indexed. Caller should re-search after.
342
+ */
343
+ export function autoIndexFromCwd(
344
+ store: KnowledgeStore,
345
+ queries: string[],
346
+ cwd: string,
347
+ owner: { ownerScope: "session"; ownerId: string },
348
+ ): { chunksIndexed: number; filesIndexed: number; filesScanned: number } {
349
+ const terms = extractIndexTerms(queries);
350
+ if (terms.length === 0) return { chunksIndexed: 0, filesIndexed: 0, filesScanned: 0 };
351
+
352
+ const matched: string[] = [];
353
+ let filesScanned = 0;
354
+
355
+ const walk = (dir: string, depth: number): void => {
356
+ if (depth > AUTO_INDEX_MAX_DEPTH) return;
357
+ if (matched.length >= AUTO_INDEX_MAX_FILES) return;
358
+ if (filesScanned >= AUTO_INDEX_MAX_SCAN) return;
359
+
360
+ let entries;
361
+ try {
362
+ entries = readdirSync(dir, { withFileTypes: true });
363
+ } catch {
364
+ return;
365
+ }
366
+
367
+ for (const entry of entries) {
368
+ if (matched.length >= AUTO_INDEX_MAX_FILES) return;
369
+ if (filesScanned >= AUTO_INDEX_MAX_SCAN) return;
370
+ const name = entry.name;
371
+ if (name.startsWith(".") && name !== "." && name !== "..") {
372
+ // Allow a few well-known dotted source dirs but skip caches/VCS.
373
+ if (AUTO_INDEX_SKIP_DIRS.has(name)) continue;
374
+ if (entry.isDirectory()) continue;
375
+ }
376
+ if (entry.isDirectory()) {
377
+ if (AUTO_INDEX_SKIP_DIRS.has(name)) continue;
378
+ walk(join(dir, name), depth + 1);
379
+ continue;
380
+ }
381
+ if (!entry.isFile()) continue;
382
+ const ext = extname(name).toLowerCase();
383
+ if (!AUTO_INDEX_EXTENSIONS.has(ext)) continue;
384
+ const full = join(dir, name);
385
+ filesScanned++;
386
+ let body: string;
387
+ try {
388
+ const stat = statSync(full);
389
+ if (stat.size > AUTO_INDEX_MAX_FILE_BYTES) continue;
390
+ body = readFileSync(full, "utf8");
391
+ } catch {
392
+ continue;
393
+ }
394
+ const lower = body.toLowerCase();
395
+ if (terms.some((t) => lower.includes(t))) {
396
+ matched.push(full);
397
+ }
398
+ }
399
+ };
400
+
401
+ walk(cwd, 0);
402
+
403
+ if (matched.length === 0) {
404
+ return { chunksIndexed: 0, filesIndexed: 0, filesScanned };
405
+ }
406
+
407
+ let chunksIndexed = 0;
408
+ for (const file of matched) {
409
+ let body: string;
410
+ try {
411
+ body = readFileSync(file, "utf8");
412
+ } catch {
413
+ continue;
414
+ }
415
+ const rel = file.startsWith(cwd) ? file.slice(cwd.length).replace(/^[\\/]+/, "") : file;
416
+ const source = `auto-index:${rel}`;
417
+ // Wrap with a markdown title so the chunker assigns a useful heading.
418
+ const chunks = chunkMarkdown(`# ${rel}\n\n${body}`, source);
419
+ if (chunks.length === 0) continue;
420
+ store.index(chunks, source, owner);
421
+ chunksIndexed += chunks.length;
422
+ }
423
+
424
+ return { chunksIndexed, filesIndexed: matched.length, filesScanned };
425
+ }
426
+
427
+ export interface RegisterContextModeToolsOptions {
428
+ repomap?: { enabled?: boolean; tokenBudget: number; maxFiles: number };
429
+ knowledgeToolsEnabled?: boolean;
430
+ }
431
+
432
+ export function registerContextModeTools(
433
+ platform: Platform,
434
+ storeProvider: KnowledgeStoreProvider,
435
+ options: RegisterContextModeToolsOptions = {},
436
+ ): void {
154
437
  if (!platform.registerTool) return;
155
438
 
156
439
  const languages = getSupportedLanguages();
440
+ const knowledgeToolsEnabled = options.knowledgeToolsEnabled !== false;
157
441
 
158
442
  // ── ctx_execute ────────────────────────────────────────────
159
443
  platform.registerTool({
@@ -187,7 +471,7 @@ export function registerContextModeTools(platform: Platform, store: KnowledgeSto
187
471
  if (result.exitCode !== 0) output += `\n[exit code: ${result.exitCode}]`;
188
472
 
189
473
  const source = `ctx_execute:${language}:${Date.now()}`;
190
- output = maybeFilterByIntent(output, intent, source, store);
474
+ output = maybeFilterByIntent(output, intent, source, resolveKnowledgeStore(storeProvider));
191
475
 
192
476
  trackCall("ctx_execute", output.length);
193
477
  return { content: [{ type: "text", text: capResponseSize(output) }] };
@@ -213,12 +497,51 @@ export function registerContextModeTools(platform: Platform, store: KnowledgeSto
213
497
  code: { type: "string", description: "Code to process FILE_CONTENT. Print summary via console.log/print/echo." },
214
498
  intent: { type: "string", description: "What you're looking for in the output" },
215
499
  timeout: { type: "number", description: "Max execution time in ms (default: 30000)" },
500
+ cache: { type: "boolean", description: "Opt into request caching for deterministic file-processing calls" },
501
+ cacheTtlMs: { type: "number", description: "Request cache TTL in milliseconds (default: 300000)" },
216
502
  },
217
503
  required: ["path", "language", "code"],
218
504
  },
219
505
  async execute(_toolCallId: string, params: any) {
220
- const { path: filePath, language, code, intent, timeout } = params;
221
- const fileContent = readFileSync(filePath, "utf-8");
506
+ const { path: filePath, language, code, intent, timeout, cache, cacheTtlMs } = params;
507
+ const nativeFilePath = resolveNativeFilePath(String(filePath));
508
+ const canonicalFilePath = canonicalizeSourcePath(nativeFilePath, process.cwd());
509
+ const fileContent = readFileSync(nativeFilePath, "utf-8");
510
+ const fileHash = sha256Text(fileContent);
511
+ const cacheStore = cache === true ? getCacheStore() : null;
512
+ const requestCacheArgs = {
513
+ path: canonicalFilePath,
514
+ language,
515
+ codeHash: sha256Text(String(code)),
516
+ timeout: timeout ?? null,
517
+ intent: typeof intent === "string" ? intent : null,
518
+ fileHash,
519
+ };
520
+ const argsHash = sha256Text(stableStringify(requestCacheArgs));
521
+ const fingerprint = fileHash;
522
+ if (cacheStore) {
523
+ const cached = cacheStore.getRequestCache({
524
+ tool: "ctx_execute_file",
525
+ argsHash,
526
+ cwd: process.cwd(),
527
+ fingerprint,
528
+ });
529
+ if (cached.hit) {
530
+ const opened = cacheStore.openText(cached.handle);
531
+ if (opened.ok) {
532
+ const output = `[cache hit: ${cached.handle}]\n${opened.text}`;
533
+ recordRequestCacheMetric({
534
+ tool: "ctx_execute_file",
535
+ beforeBytes: opened.meta.sizeBytes,
536
+ afterBytes: byteLength(output),
537
+ cacheHit: 1,
538
+ });
539
+ trackCall("ctx_execute_file", output.length);
540
+ return { content: [{ type: "text", text: capResponseSize(output) }] };
541
+ }
542
+ }
543
+ }
544
+
222
545
  const augmentedCode = injectFileContent(language, fileContent, code);
223
546
 
224
547
  const result = await executeCode(language, augmentedCode, { timeout });
@@ -227,14 +550,38 @@ export function registerContextModeTools(platform: Platform, store: KnowledgeSto
227
550
  if (result.stderr) output += `\n[stderr]\n${result.stderr}`;
228
551
  if (result.exitCode !== 0) output += `\n[exit code: ${result.exitCode}]`;
229
552
 
230
- const source = `ctx_execute_file:${filePath}:${Date.now()}`;
231
- output = maybeFilterByIntent(output, intent, source, store);
553
+ const source = `ctx_execute_file:${canonicalFilePath}:${Date.now()}`;
554
+ output = maybeFilterByIntent(output, intent, source, resolveKnowledgeStore(storeProvider));
555
+ if (cacheStore && result.exitCode === 0) {
556
+ const meta = cacheStore.putText({
557
+ sessionId: getSessionId(),
558
+ text: output,
559
+ sourceTool: "ctx_execute_file",
560
+ sourceHash: argsHash,
561
+ recordMetric: false,
562
+ });
563
+ cacheStore.putRequestCache({
564
+ tool: "ctx_execute_file",
565
+ argsHash,
566
+ cwd: process.cwd(),
567
+ fingerprint,
568
+ handle: meta.handle,
569
+ ttlMs: typeof cacheTtlMs === "number" ? cacheTtlMs : 5 * 60 * 1000,
570
+ });
571
+ recordRequestCacheMetric({
572
+ tool: "ctx_execute_file",
573
+ beforeBytes: byteLength(output),
574
+ afterBytes: byteLength(output),
575
+ cacheHit: 0,
576
+ });
577
+ }
232
578
 
233
579
  trackCall("ctx_execute_file", output.length);
234
580
  return { content: [{ type: "text", text: capResponseSize(output) }] };
235
581
  },
236
582
  });
237
583
 
584
+ if (knowledgeToolsEnabled) {
238
585
  // ── ctx_batch_execute ──────────────────────────────────────
239
586
  platform.registerTool({
240
587
  name: "ctx_batch_execute",
@@ -272,6 +619,7 @@ export function registerContextModeTools(platform: Platform, store: KnowledgeSto
272
619
  required: ["commands", "queries"],
273
620
  },
274
621
  async execute(_toolCallId: string, params: any) {
622
+ const store = requireKnowledgeStore(storeProvider);
275
623
  const { commands, queries, timeout = 60000 } = params;
276
624
  const sections: string[] = [];
277
625
 
@@ -284,13 +632,13 @@ export function registerContextModeTools(platform: Platform, store: KnowledgeSto
284
632
  const source = `batch:${cmd.label}`;
285
633
  const chunks = chunkMarkdown(output, source);
286
634
  if (chunks.length > 0) {
287
- store.index(chunks, source);
635
+ store.index(chunks, source, currentKnowledgeOwner());
288
636
  }
289
637
  sections.push(`- ${cmd.label} (${(output.length / 1024).toFixed(1)}KB)`);
290
638
  }
291
639
 
292
640
  // Search across all indexed content
293
- const results = store.search(queries, { limit: 5 });
641
+ const results = store.search(queries, { limit: 5, owner: currentKnowledgeOwner() });
294
642
 
295
643
  let text = `Executed ${commands.length} commands. Indexed ${sections.length} sections.\n\n`;
296
644
  text += `## Indexed Sections\n\n${sections.join("\n")}\n\n`;
@@ -307,7 +655,9 @@ export function registerContextModeTools(platform: Platform, store: KnowledgeSto
307
655
  return { content: [{ type: "text", text: capResponseSize(text) }] };
308
656
  },
309
657
  });
658
+ }
310
659
 
660
+ if (knowledgeToolsEnabled) {
311
661
  // ── ctx_index ──────────────────────────────────────────────
312
662
  platform.registerTool({
313
663
  name: "ctx_index",
@@ -315,6 +665,11 @@ export function registerContextModeTools(platform: Platform, store: KnowledgeSto
315
665
  description:
316
666
  "Index documentation or knowledge content into a searchable BM25 knowledge base. After indexing, use ctx_search to retrieve specific sections on-demand.",
317
667
  promptSnippet: "ctx_index — store content in knowledge base for later search",
668
+ promptGuidelines: [
669
+ "Use when you have raw text or markdown to make searchable later via ctx_search",
670
+ "Use ctx_fetch_and_index instead when the source is a URL",
671
+ "Provide a descriptive `source` label so others can scope ctx_search by source",
672
+ ],
318
673
  parameters: {
319
674
  type: "object",
320
675
  properties: {
@@ -325,6 +680,7 @@ export function registerContextModeTools(platform: Platform, store: KnowledgeSto
325
680
  required: ["source"],
326
681
  },
327
682
  async execute(_toolCallId: string, params: any) {
683
+ const store = requireKnowledgeStore(storeProvider);
328
684
  const { content, path: filePath, source } = params;
329
685
 
330
686
  if ((content && filePath) || (!content && !filePath)) {
@@ -333,21 +689,29 @@ export function registerContextModeTools(platform: Platform, store: KnowledgeSto
333
689
 
334
690
  const text = filePath ? readFileSync(filePath, "utf-8") : content;
335
691
  const chunks = chunkMarkdown(text, source);
336
- store.index(chunks, source);
692
+ store.index(chunks, source, currentKnowledgeOwner());
337
693
 
338
694
  const output = `Indexed ${chunks.length} chunks under source "${source}".`;
339
695
  trackCall("ctx_index", output.length);
340
696
  return { content: [{ type: "text", text: output }] };
341
697
  },
342
698
  });
699
+ }
343
700
 
701
+ if (knowledgeToolsEnabled) {
344
702
  // ── ctx_search ─────────────────────────────────────────────
345
703
  platform.registerTool({
346
704
  name: "ctx_search",
347
705
  label: "Search Knowledge Base",
348
706
  description:
349
- "Search indexed content. Requires prior indexing via ctx_batch_execute, ctx_index, or ctx_fetch_and_index. Pass ALL search questions as queries array in ONE call.",
707
+ "Search indexed content. Auto-bootstraps the knowledge store from the project on the first call when nothing is indexed yet subsequent calls skip auto-indexing.",
350
708
  promptSnippet: "ctx_search — query indexed content with BM25 search",
709
+ promptGuidelines: [
710
+ "Use to retrieve specific sections from previously indexed content",
711
+ "Pass ALL questions in the queries array in one call — do not chain ctx_search calls",
712
+ "Filter by `source` when you know which indexed bundle to search",
713
+ "Safe to call on a fresh session: the first call bootstraps an index from the project when none exists",
714
+ ],
351
715
  parameters: {
352
716
  type: "object",
353
717
  properties: {
@@ -362,16 +726,193 @@ export function registerContextModeTools(platform: Platform, store: KnowledgeSto
362
726
  },
363
727
  required: ["queries"],
364
728
  },
365
- async execute(_toolCallId: string, params: any) {
729
+ async execute(_toolCallId: string, params: any, _abortSignal?: any, _onUpdate?: any, ctx?: any) {
730
+ const store = requireKnowledgeStore(storeProvider);
366
731
  const { queries, source, contentType, limit } = params;
367
- const results = store.search(queries, { source, contentType, limit });
368
- const output = formatSearchResults(results);
732
+ const owner = currentKnowledgeOwner();
733
+
734
+ let results = store.search(queries, { source, contentType, limit, owner });
735
+ let bootstrapNote = "";
369
736
 
737
+ const allEmpty = results.every((g) => g.results.length === 0);
738
+ const sessionKey = `${owner.ownerId}|${source ?? ""}`;
739
+ const canBootstrap =
740
+ allEmpty &&
741
+ Array.isArray(queries) &&
742
+ queries.length > 0 &&
743
+ !source &&
744
+ !_autoIndexAttempted.has(sessionKey);
745
+
746
+ if (canBootstrap) {
747
+ _autoIndexAttempted.add(sessionKey);
748
+ const stats = store.getStats();
749
+ if (stats.totalChunks === 0) {
750
+ const cwd = typeof ctx?.cwd === "string" && ctx.cwd.length > 0 ? ctx.cwd : process.cwd();
751
+ let bootstrap;
752
+ try {
753
+ bootstrap = autoIndexFromCwd(store, queries, cwd, owner);
754
+ } catch {
755
+ bootstrap = { chunksIndexed: 0, filesIndexed: 0, filesScanned: 0 };
756
+ }
757
+ if (bootstrap.chunksIndexed > 0) {
758
+ results = store.search(queries, { source, contentType, limit, owner });
759
+ bootstrapNote =
760
+ `[auto-indexed ${bootstrap.filesIndexed} files (${bootstrap.chunksIndexed} chunks) ` +
761
+ `from ${bootstrap.filesScanned} scanned to bootstrap the empty knowledge store]\n\n`;
762
+ } else if (bootstrap.filesScanned > 0) {
763
+ bootstrapNote =
764
+ `[scanned ${bootstrap.filesScanned} files but none matched the query terms; ` +
765
+ `use ctx_batch_execute or ctx_index to index relevant content explicitly]\n\n`;
766
+ }
767
+ }
768
+ }
769
+
770
+ const output = bootstrapNote + formatSearchResults(results);
370
771
  trackCall("ctx_search", output.length);
371
772
  return { content: [{ type: "text", text: capResponseSize(output) }] };
372
773
  },
373
774
  });
775
+ }
776
+
777
+ // ── ctx_repomap ─────────────────────────────────────────────
778
+ if (options.repomap?.enabled !== false) {
779
+ platform.registerTool({
780
+ name: "ctx_repomap",
781
+ label: "Repository Map",
782
+ description: "Build a deterministic structural repository map capped by an estimated token budget.",
783
+ promptSnippet: "ctx_repomap — structural repository map with symbols/imports",
784
+ promptGuidelines: [
785
+ "Use before broad code exploration when you need a bounded overview",
786
+ "Pass focus files to personalize ranking toward the current task",
787
+ ],
788
+ parameters: {
789
+ type: "object",
790
+ properties: {
791
+ focus: { type: "array", items: { type: "string" }, description: "Optional focus files for personalized ranking" },
792
+ tokenBudget: { type: "number", description: "Estimated token budget for the emitted map (default: 4000)" },
793
+ },
794
+ },
795
+ async execute(_toolCallId: string, params: any, _abortSignal?: any, _onUpdate?: any, ctx?: any) {
796
+ const cwd = typeof ctx?.cwd === "string" ? ctx.cwd : process.cwd();
797
+ const result = await buildRepoMap(platform, {
798
+ cwd,
799
+ focus: Array.isArray(params?.focus) ? params.focus : [],
800
+ tokenBudget: typeof params?.tokenBudget === "number" ? params.tokenBudget : options.repomap?.tokenBudget,
801
+ maxFiles: options.repomap?.maxFiles,
802
+ });
803
+ const output = capResponseSize(result.text);
804
+ const outputBytes = byteLength(output);
805
+ recordL4RetrievalMetric("ctx_repomap", result.emittedSourceBytes, outputBytes);
806
+ trackCall("ctx_repomap", output.length);
807
+ return { content: [{ type: "text", text: output }] };
808
+ },
809
+ });
810
+ }
811
+
812
+ // ── ctx_symbol ──────────────────────────────────────────────
813
+ platform.registerTool({
814
+ name: "ctx_symbol",
815
+ label: "Symbol Summary",
816
+ description: "Capability-gated facade for native LSP symbol summaries. Returns an explicit diagnostic when the platform has no callable LSP API.",
817
+ promptSnippet: "ctx_symbol — bounded symbol summary when platform LSP facade is available",
818
+ promptGuidelines: [
819
+ "Use native lsp directly when this reports platform_lsp_facade_unavailable",
820
+ ],
821
+ parameters: {
822
+ type: "object",
823
+ properties: {
824
+ query: { type: "string", description: "Symbol name or query" },
825
+ action: { type: "string", enum: ["definition", "references", "hover", "symbols"], description: "LSP action" },
826
+ },
827
+ required: ["query"],
828
+ },
829
+ async execute() {
830
+ const text = [
831
+ "platform_lsp_facade_unavailable",
832
+ "The current OMP Platform adapter does not expose a callable native LSP/tool API to extensions.",
833
+ "Use the native lsp tool directly for definitions, references, hover, and symbols.",
834
+ ].join("\n");
835
+ const diagnosticBytes = byteLength(text);
836
+ recordL4RetrievalMetric("ctx_symbol", diagnosticBytes, diagnosticBytes);
837
+ trackCall("ctx_symbol", text.length);
838
+ return { content: [{ type: "text", text }] };
839
+ },
840
+ });
374
841
 
842
+ // ── ctx_open_cached ────────────────────────────────────────
843
+ platform.registerTool({
844
+ name: "ctx_open_cached",
845
+ label: "Open Cached Context Handle",
846
+ description:
847
+ "Open a cache://<sha256> handle and return a bounded slice of cached text with offset metadata.",
848
+ promptSnippet: "ctx_open_cached — open a cache:// handle with bounded offset/limit reads",
849
+ promptGuidelines: [
850
+ "Use when the user provides a cache:// handle or asks to open cached content",
851
+ "Always use offset/limit for follow-up reads instead of requesting the whole payload",
852
+ "Invalid, missing, or corrupt handles return explicit text instead of throwing",
853
+ ],
854
+ parameters: {
855
+ type: "object",
856
+ properties: {
857
+ handle: { type: "string", description: "cache://<sha256> handle to open" },
858
+ offset: { type: "number", description: "Character offset in decoded cached text (default: 0)" },
859
+ limit: { type: "number", description: "Maximum characters to return, capped at 100KB characters" },
860
+ },
861
+ required: ["handle"],
862
+ },
863
+ async execute(_toolCallId: string, params: any) {
864
+ const parsed = parseCacheHandle(String(params?.handle ?? ""));
865
+ if (!parsed.ok) {
866
+ const output = `Cannot open cached content: ${parsed.message}.`;
867
+ recordCacheOpenMetric({ beforeBytes: 0, afterBytes: byteLength(output), cacheHit: 0 });
868
+ trackCall("ctx_open_cached", output.length);
869
+ return { content: [{ type: "text", text: capResponseSize(output) }] };
870
+ }
871
+
872
+ const cacheStore = getCacheStore();
873
+ if (!cacheStore) {
874
+ const output = "Cannot open cached content: cache store is unavailable for this session.";
875
+ recordCacheOpenMetric({ beforeBytes: 0, afterBytes: byteLength(output), cacheHit: 0 });
876
+ trackCall("ctx_open_cached", output.length);
877
+ return { content: [{ type: "text", text: output }] };
878
+ }
879
+
880
+ const opened = cacheStore.openText(parsed.handle);
881
+ if (!opened.ok) {
882
+ recordCacheOpenMetric({ beforeBytes: 0, afterBytes: byteLength(opened.message), cacheHit: 0 });
883
+ trackCall("ctx_open_cached", opened.message.length);
884
+ return { content: [{ type: "text", text: capResponseSize(opened.message) }] };
885
+ }
886
+
887
+ // Reserve headroom for the metadata block so capResponseSize never silently drops
888
+ // characters that we already advertised in `Returned`/`Next offset`.
889
+ const HEADER_HEADROOM_CHARS = 512;
890
+ const userLimit = typeof params?.limit === "number" ? params.limit : undefined;
891
+ const responseBudget = Math.max(0, MAX_RESPONSE_SIZE - HEADER_HEADROOM_CHARS);
892
+ const cappedLimit = userLimit === undefined
893
+ ? responseBudget
894
+ : Math.min(userLimit, responseBudget);
895
+ const slice = sliceCachedText(opened.text, params?.offset, cappedLimit);
896
+ const end = slice.offset + slice.returnedChars;
897
+ let output = `## Cached content ${opened.handle}\n\n`;
898
+ output += `- Total: ${opened.meta.sizeBytes} bytes, ${slice.totalChars} chars\n`;
899
+ output += `- Returned: chars ${slice.offset}..${end} of ${slice.totalChars}\n`;
900
+ if (slice.nextOffset !== null) {
901
+ output += `- Next offset: ${slice.nextOffset}\n`;
902
+ }
903
+ output += `\n---\n${slice.text}`;
904
+
905
+ recordCacheOpenMetric({
906
+ beforeBytes: opened.meta.sizeBytes,
907
+ afterBytes: byteLength(slice.text),
908
+ cacheHit: 1,
909
+ });
910
+ trackCall("ctx_open_cached", output.length);
911
+ return { content: [{ type: "text", text: capResponseSize(output) }] };
912
+ },
913
+ });
914
+
915
+ if (knowledgeToolsEnabled) {
375
916
  // ── ctx_fetch_and_index ────────────────────────────────────
376
917
  platform.registerTool({
377
918
  name: "ctx_fetch_and_index",
@@ -379,6 +920,10 @@ export function registerContextModeTools(platform: Platform, store: KnowledgeSto
379
920
  description:
380
921
  "Fetch URL content, convert HTML to markdown, index into searchable knowledge base, and return a ~3KB preview. Use ctx_search for deeper lookups.",
381
922
  promptSnippet: "ctx_fetch_and_index — fetch URL, index content, return preview",
923
+ promptGuidelines: [
924
+ "Use this instead of curl/wget/WebFetch — raw HTML never enters context",
925
+ "After indexing, use ctx_search for deeper lookups beyond the preview",
926
+ ],
382
927
  parameters: {
383
928
  type: "object",
384
929
  properties: {
@@ -389,8 +934,9 @@ export function registerContextModeTools(platform: Platform, store: KnowledgeSto
389
934
  required: ["url"],
390
935
  },
391
936
  async execute(_toolCallId: string, params: any) {
937
+ const store = requireKnowledgeStore(storeProvider);
392
938
  const { url, source, force } = params;
393
- const result = await fetchAndIndex(url, store, { source, force });
939
+ const result = await fetchAndIndex(url, store, { source, force, owner: currentKnowledgeOwner() });
394
940
 
395
941
  let output = result.preview;
396
942
  if (!result.cached) {
@@ -403,6 +949,7 @@ export function registerContextModeTools(platform: Platform, store: KnowledgeSto
403
949
  return { content: [{ type: "text", text: capResponseSize(output) }] };
404
950
  },
405
951
  });
952
+ }
406
953
 
407
954
  // ── ctx_stats ──────────────────────────────────────────────
408
955
  platform.registerTool({
@@ -411,9 +958,72 @@ export function registerContextModeTools(platform: Platform, store: KnowledgeSto
411
958
  description:
412
959
  "Returns context consumption statistics for the current session. Shows total bytes returned to context, breakdown by tool, call counts, and knowledge base stats.",
413
960
  promptSnippet: "ctx_stats — show session context stats",
414
- parameters: { type: "object", properties: {} },
415
- async execute() {
416
- const storeStats = store.getStats();
961
+ promptGuidelines: [
962
+ "Use only when the user asks about context consumption or to debug context bloat",
963
+ "Do not call this proactively — it consumes context itself",
964
+ ],
965
+ parameters: {
966
+ type: "object",
967
+ properties: {
968
+ format: {
969
+ type: "string",
970
+ enum: ["markdown", "json"],
971
+ description: "Output format. Defaults to markdown for human reading; json is for tokscale-class consumers.",
972
+ },
973
+ },
974
+ },
975
+ async execute(_id: string, params: any = {}) {
976
+ const store = resolveKnowledgeStore(storeProvider);
977
+ const format = params?.format === "json" ? "json" : "markdown";
978
+ const metricsStore = getMetricsStore();
979
+ const sessionId = getSessionId();
980
+
981
+ if (format === "json") {
982
+ const meta = (() => {
983
+ try {
984
+ return metricsStore?.getSessionMeta(sessionId) ?? null;
985
+ } catch {
986
+ return null;
987
+ }
988
+ })();
989
+ const totals = metricsStore
990
+ ? metricsStore.getSessionTotals(sessionId)
991
+ : { beforeBytes: 0, afterBytes: 0, saved: 0, rowCount: 0 };
992
+ const perProcessor = metricsStore ? metricsStore.getTopProcessors(sessionId, 50) : [];
993
+ const perLayer = metricsStore ? metricsStore.getPerLayer(sessionId) : [];
994
+ const uniqueSourceShare = metricsStore
995
+ ? metricsStore.getUniqueSourceShare(sessionId)
996
+ : 0;
997
+ const writeFailures = metricsStore
998
+ ? metricsStore.getSessionWriteFailures(sessionId)
999
+ : 0;
1000
+ const tokensEstimated = Math.ceil(totals.saved / 4);
1001
+ const payload = {
1002
+ session: {
1003
+ id: sessionId,
1004
+ startedAt: meta?.started_at ?? 0,
1005
+ rowCount: totals.rowCount,
1006
+ },
1007
+ totals: {
1008
+ beforeBytes: totals.beforeBytes,
1009
+ afterBytes: totals.afterBytes,
1010
+ saved: totals.saved,
1011
+ tokensEstimated,
1012
+ },
1013
+ perProcessor,
1014
+ perLayer,
1015
+ uniqueSourceShare,
1016
+ writeFailures,
1017
+ };
1018
+ const text = JSON.stringify(payload, null, 2);
1019
+ trackCall("ctx_stats", text.length);
1020
+ return { content: [{ type: "text", text }] };
1021
+ }
1022
+
1023
+ // Markdown (default; existing behavior preserved).
1024
+ const storeStats = store
1025
+ ? store.getStats()
1026
+ : { totalChunks: 0, sources: [], dbSizeBytes: 0 };
417
1027
  const totalCalls = Object.values(stats.calls).reduce((sum, n) => sum + n, 0);
418
1028
  const estimatedTokens = Math.ceil(stats.bytesReturned / 4); // rough estimate
419
1029
 
@@ -433,11 +1043,23 @@ export function registerContextModeTools(platform: Platform, store: KnowledgeSto
433
1043
  output += `- **Sources**: ${storeStats.sources.length > 0 ? storeStats.sources.join(", ") : "(none)"}\n`;
434
1044
  output += `- **DB size**: ${(storeStats.dbSizeBytes / 1024).toFixed(1)}KB\n`;
435
1045
 
1046
+ // Append a Savings panel for the active metrics session, when available.
1047
+ if (metricsStore) {
1048
+ const totals = metricsStore.getSessionTotals(sessionId);
1049
+ const uniqueSourceShare = metricsStore.getUniqueSourceShare(sessionId);
1050
+ output += `\n### Session savings (L1 metrics)\n\n`;
1051
+ output += `- **Before**: ${(totals.beforeBytes / 1024).toFixed(1)}KB\n`;
1052
+ output += `- **After**: ${(totals.afterBytes / 1024).toFixed(1)}KB\n`;
1053
+ output += `- **Saved**: ${(totals.saved / 1024).toFixed(1)}KB\n`;
1054
+ output += `- **Unique-source share**: ${Math.round(uniqueSourceShare * 100)}%\n`;
1055
+ }
1056
+
436
1057
  trackCall("ctx_stats", output.length);
437
1058
  return { content: [{ type: "text", text: output }] };
438
1059
  },
439
1060
  });
440
1061
 
1062
+ if (knowledgeToolsEnabled) {
441
1063
  // ── ctx_purge ──────────────────────────────────────────────
442
1064
  platform.registerTool({
443
1065
  name: "ctx_purge",
@@ -445,14 +1067,24 @@ export function registerContextModeTools(platform: Platform, store: KnowledgeSto
445
1067
  description:
446
1068
  "Delete all indexed content from the knowledge base. Does NOT touch the event store (events.db). Use when you want a fresh start.",
447
1069
  promptSnippet: "ctx_purge — clear all indexed content",
1070
+ promptGuidelines: [
1071
+ "Use only when the user explicitly asks to reset the knowledge base",
1072
+ "Does NOT delete the event store (events.db) — only the knowledge index",
1073
+ ],
448
1074
  parameters: { type: "object", properties: {} },
449
1075
  async execute() {
1076
+ const store = requireKnowledgeStore(storeProvider);
450
1077
  const count = store.purge();
1078
+ // Mark the current session as bootstrap-attempted so a follow-up
1079
+ // ctx_search does not undo the explicit purge by re-bootstrapping.
1080
+ const owner = currentKnowledgeOwner();
1081
+ _autoIndexAttempted.add(`${owner.ownerId}|`);
451
1082
  const output = `Purged ${count} chunks from knowledge base.`;
452
1083
  trackCall("ctx_purge", output.length);
453
1084
  return { content: [{ type: "text", text: output }] };
454
1085
  },
455
1086
  });
1087
+ }
456
1088
  }
457
1089
 
458
1090
  // Exported for testing