@hassangameryt/freeflow 0.4.0 → 0.6.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 (285) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +2 -1
  3. package/.codex-plugin/plugin.json +2 -1
  4. package/README.md +150 -155
  5. package/capabilities/cognitive-routing/SKILL.md +229 -0
  6. package/capabilities/context-virtualization/SKILL.md +103 -0
  7. package/capabilities/conversation-history/SKILL.md +53 -0
  8. package/command-surface.json +17 -13
  9. package/hooks/freeflow-runtime-context.mjs +393 -159
  10. package/hooks/hooks.json +26 -1
  11. package/hooks/tests/check-runtime-context-hook.sh +223 -54
  12. package/package.json +25 -16
  13. package/pi-extension/dist/cognitive-routing/commands.js +96 -0
  14. package/pi-extension/dist/cognitive-routing/config.js +236 -0
  15. package/pi-extension/dist/cognitive-routing/controller.js +862 -0
  16. package/pi-extension/dist/cognitive-routing/history.js +439 -0
  17. package/pi-extension/dist/cognitive-routing/host.js +126 -0
  18. package/pi-extension/dist/cognitive-routing/runtime.js +43 -0
  19. package/pi-extension/dist/cognitive-routing/tool.js +211 -0
  20. package/pi-extension/dist/cognitive-routing/types.js +6 -0
  21. package/pi-extension/dist/context-virtualization/commands.js +89 -0
  22. package/pi-extension/dist/context-virtualization/projector.js +42 -0
  23. package/pi-extension/dist/context-virtualization/runtime.js +356 -0
  24. package/pi-extension/dist/context-virtualization/state.js +57 -0
  25. package/pi-extension/dist/context-virtualization/types.js +10 -0
  26. package/pi-extension/dist/conversation-history/passages.js +66 -0
  27. package/pi-extension/dist/conversation-history/ranking.js +105 -0
  28. package/pi-extension/dist/conversation-history/runtime.js +226 -0
  29. package/pi-extension/dist/conversation-history/search.js +56 -0
  30. package/pi-extension/dist/conversation-history/types.js +1 -0
  31. package/pi-extension/dist/freeflow-context/resolver.js +120 -0
  32. package/pi-extension/dist/freeflow-context/runtime.js +80 -0
  33. package/pi-extension/dist/freeflow-context/source-projector.js +95 -0
  34. package/pi-extension/dist/freeflow-context/tool.js +308 -0
  35. package/pi-extension/dist/freeflow-context/types.js +12 -0
  36. package/pi-extension/dist/freeflow-context/visibility.js +28 -0
  37. package/pi-extension/dist/index.js +516 -98
  38. package/pi-extension/dist/runtime/runtime-context.js +318 -271
  39. package/pi-extension/dist/runtime/runtime-identity.js +5 -0
  40. package/pi-extension/dist/settings/settings-tui.js +78 -0
  41. package/pi-extension/dist/settings/settings-ui.js +593 -563
  42. package/pi-extension/dist/ui/text-component.js +57 -0
  43. package/pi-extension/src/cognitive-routing/commands.ts +119 -0
  44. package/pi-extension/src/cognitive-routing/config.ts +307 -0
  45. package/pi-extension/src/cognitive-routing/controller.ts +1118 -0
  46. package/pi-extension/src/cognitive-routing/history.ts +638 -0
  47. package/pi-extension/src/cognitive-routing/host.ts +172 -0
  48. package/pi-extension/src/cognitive-routing/runtime.ts +57 -0
  49. package/pi-extension/src/cognitive-routing/tool.ts +272 -0
  50. package/pi-extension/src/cognitive-routing/types.ts +89 -0
  51. package/pi-extension/src/context-virtualization/commands.ts +111 -0
  52. package/pi-extension/src/context-virtualization/projector.ts +62 -0
  53. package/pi-extension/src/context-virtualization/runtime.ts +406 -0
  54. package/pi-extension/src/context-virtualization/state.ts +79 -0
  55. package/pi-extension/src/context-virtualization/types.ts +92 -0
  56. package/pi-extension/src/conversation-history/passages.ts +81 -0
  57. package/pi-extension/src/conversation-history/ranking.ts +155 -0
  58. package/pi-extension/src/conversation-history/runtime.ts +250 -0
  59. package/pi-extension/src/conversation-history/search.ts +79 -0
  60. package/pi-extension/src/conversation-history/types.ts +31 -0
  61. package/pi-extension/src/freeflow-context/resolver.ts +139 -0
  62. package/pi-extension/src/freeflow-context/runtime.ts +100 -0
  63. package/pi-extension/src/freeflow-context/source-projector.ts +106 -0
  64. package/pi-extension/src/freeflow-context/tool.ts +360 -0
  65. package/pi-extension/src/freeflow-context/types.ts +70 -0
  66. package/pi-extension/src/freeflow-context/visibility.ts +53 -0
  67. package/pi-extension/src/index.ts +553 -101
  68. package/pi-extension/src/runtime/runtime-context.ts +339 -281
  69. package/pi-extension/src/runtime/runtime-identity.ts +8 -0
  70. package/pi-extension/src/settings/settings-tui.ts +93 -0
  71. package/pi-extension/src/settings/settings-ui.ts +700 -613
  72. package/pi-extension/src/ui/text-component.ts +66 -0
  73. package/pi-extension/tests/cognitive-routing/commands.test.js +134 -0
  74. package/pi-extension/tests/cognitive-routing/controller.test.js +1099 -0
  75. package/pi-extension/tests/cognitive-routing/extension-lifecycle.test.js +1071 -0
  76. package/pi-extension/tests/cognitive-routing/history.test.js +274 -0
  77. package/pi-extension/tests/cognitive-routing/host-fixture.js +4 -0
  78. package/pi-extension/tests/cognitive-routing/host-identity.test.js +63 -0
  79. package/pi-extension/tests/cognitive-routing/profile-resolution.test.js +236 -0
  80. package/pi-extension/tests/cognitive-routing/tool.test.js +230 -0
  81. package/pi-extension/tests/context-virtualization/extension.test.js +749 -0
  82. package/pi-extension/tests/context-virtualization/projector.test.js +110 -0
  83. package/pi-extension/tests/context-virtualization/runtime.test.js +216 -0
  84. package/pi-extension/tests/conversation-history/runtime.test.js +98 -0
  85. package/pi-extension/tests/conversation-history/search.test.js +97 -0
  86. package/pi-extension/tests/integration/pi-extension.test.js +928 -2047
  87. package/pi-extension/tests/integration/prompt-architecture.test.js +305 -0
  88. package/runtime/prompts/cognitive-routing.md +9 -0
  89. package/runtime/prompts/context-virtualization.md +3 -0
  90. package/runtime/prompts/conversation-history.md +3 -0
  91. package/runtime/prompts/core.md +13 -0
  92. package/runtime/prompts/interaction-contract.md +7 -0
  93. package/runtime/prompts/skills.md +65 -0
  94. package/skills/action-selection/SKILL.md +91 -0
  95. package/skills/bypass/SKILL.md +3 -2
  96. package/skills/commit-work/SKILL.md +1 -1
  97. package/skills/commit-work/references/staging-decisions.md +1 -1
  98. package/skills/decision-gate/SKILL.md +1 -1
  99. package/skills/design-for-depth/SKILL.md +14 -33
  100. package/skills/design-for-depth/references/design-pressure-signals.md +1 -1
  101. package/skills/design-for-depth/references/interface-design-loop.md +1 -1
  102. package/skills/design-for-depth/references/software-design-philosophy.md +1 -1
  103. package/skills/diagnose-failure/SKILL.md +20 -14
  104. package/skills/diagnose-failure/references/diagnostic-loop-catalog.md +1 -3
  105. package/skills/diagnose-failure/references/{flaky-and-performance.md → flaky-and-performance-diagnosis.md} +1 -1
  106. package/skills/discuss/SKILL.md +34 -22
  107. package/skills/discuss/references/discussion-continuity.md +71 -0
  108. package/skills/evaluate-skill/SKILL.md +5 -5
  109. package/skills/evaluate-skill/references/{definition-schema.md → evaluation-definition-schema.md} +42 -2
  110. package/skills/evaluate-skill/references/execution-and-evidence.md +8 -3
  111. package/skills/evaluate-skill/scripts/lib/definitions.mjs +114 -3
  112. package/skills/evaluate-skill/scripts/lib/grade-interaction.mjs +156 -1
  113. package/skills/evaluate-skill/scripts/lib/grade.mjs +9 -1
  114. package/skills/evaluate-skill/scripts/lib/pi.mjs +147 -32
  115. package/skills/evaluate-skill/scripts/lib/rpc.mjs +4 -4
  116. package/skills/evaluate-skill/scripts/lib/runner.mjs +36 -14
  117. package/skills/evaluate-skill/scripts/lib/sandbox.mjs +92 -0
  118. package/skills/evaluate-skill/scripts/pi-guard.mjs +1 -1
  119. package/skills/evaluate-skill/scripts/pi-observer.mjs +66 -0
  120. package/skills/evaluate-skill/scripts/skill-eval.mjs +6 -5
  121. package/skills/execute-work/SKILL.md +18 -15
  122. package/skills/execute-work/references/code-practices.md +1 -1
  123. package/skills/execute-work/references/execute-work-edges.md +42 -0
  124. package/skills/finish-branch/SKILL.md +2 -2
  125. package/skills/finish-branch/references/integration-options.md +1 -1
  126. package/skills/handoff/SKILL.md +3 -3
  127. package/skills/launch-work/SKILL.md +1 -1
  128. package/skills/launch-work/references/launch-readiness.md +2 -2
  129. package/skills/migration-work/SKILL.md +2 -2
  130. package/skills/migration-work/references/migration-lifecycle.md +1 -1
  131. package/skills/mode-contract/SKILL.md +32 -10
  132. package/skills/release-work/SKILL.md +1 -1
  133. package/skills/release-work/references/release-evidence.md +1 -1
  134. package/skills/review-artifact/SKILL.md +28 -26
  135. package/skills/review-work/SKILL.md +28 -26
  136. package/skills/review-work/references/{reviewer-prompt.md → independent-work-reviewer-contract.md} +1 -1
  137. package/skills/review-work/references/security-risk-lens.md +1 -1
  138. package/skills/setup-freeflow/SKILL.md +6 -10
  139. package/skills/setup-freeflow/references/activation-contract.md +5 -10
  140. package/skills/setup-freeflow/references/host-setup.md +3 -7
  141. package/skills/simplify-code/SKILL.md +1 -1
  142. package/skills/simplify-code/references/simplification-patterns.md +1 -1
  143. package/skills/tdd/SKILL.md +18 -38
  144. package/skills/tdd/references/test-design.md +2 -2
  145. package/skills/track-work/SKILL.md +362 -58
  146. package/skills/track-work/scripts/lib/codec.mjs +570 -0
  147. package/skills/track-work/scripts/lib/commands.mjs +568 -0
  148. package/skills/track-work/scripts/lib/contract.mjs +303 -0
  149. package/skills/track-work/scripts/lib/model.mjs +506 -0
  150. package/skills/track-work/scripts/lib/operations.mjs +965 -0
  151. package/skills/track-work/scripts/lib/record-store.mjs +264 -0
  152. package/skills/track-work/scripts/lib/result.mjs +37 -0
  153. package/skills/track-work/scripts/lib/rewrites.mjs +740 -0
  154. package/skills/track-work/scripts/lib/schema.mjs +277 -0
  155. package/skills/track-work/scripts/lib/source-inventory.mjs +162 -0
  156. package/skills/track-work/scripts/lib/views.mjs +443 -0
  157. package/skills/track-work/scripts/lib/workspace.mjs +134 -0
  158. package/skills/track-work/scripts/working-record.mjs +122 -0
  159. package/skills/verify-work/SKILL.md +14 -2
  160. package/skills/verify-work/references/browser-runtime-evidence.md +1 -1
  161. package/skills/verify-work/references/integration-evidence.md +1 -1
  162. package/skills/verify-work/references/performance-evidence.md +1 -1
  163. package/skills/workflow/SKILL.md +105 -53
  164. package/skills/workflow/references/domain-skill-composition.md +22 -42
  165. package/skills/write-plan/SKILL.md +1 -1
  166. package/skills/write-skill/SKILL.md +3 -2
  167. package/skills/write-skill/references/progressive-disclosure.md +17 -3
  168. package/pi-extension/dist/output-router/host-producer-identification.js +0 -232
  169. package/pi-extension/dist/output-router/native-safety-net.js +0 -216
  170. package/pi-extension/dist/output-router/observed-tool-routing.js +0 -122
  171. package/pi-extension/dist/output-router/renderers.js +0 -1023
  172. package/pi-extension/dist/output-router/router-tools.js +0 -554
  173. package/pi-extension/dist/output-router/schemas.js +0 -383
  174. package/pi-extension/dist/output-router/status.js +0 -744
  175. package/pi-extension/dist/output-router/utils.js +0 -502
  176. package/pi-extension/src/output-router/host-producer-identification.ts +0 -256
  177. package/pi-extension/src/output-router/native-safety-net.ts +0 -248
  178. package/pi-extension/src/output-router/observed-tool-routing.ts +0 -134
  179. package/pi-extension/src/output-router/renderers.ts +0 -1108
  180. package/pi-extension/src/output-router/router-tools.ts +0 -579
  181. package/pi-extension/src/output-router/schemas.ts +0 -387
  182. package/pi-extension/src/output-router/status.ts +0 -791
  183. package/pi-extension/src/output-router/utils.ts +0 -554
  184. package/pi-extension/tests/output-router/observed-producers.test.js +0 -132
  185. package/pi-extension/tests/output-router/search.test.js +0 -722
  186. package/router/dist/benchmarks/benchmark-harness.d.ts +0 -35
  187. package/router/dist/benchmarks/benchmark-harness.js +0 -92
  188. package/router/dist/benchmarks/benchmarks.d.ts +0 -109
  189. package/router/dist/benchmarks/benchmarks.js +0 -979
  190. package/router/dist/benchmarks/codex-qa-benchmarks.d.ts +0 -95
  191. package/router/dist/benchmarks/codex-qa-benchmarks.js +0 -500
  192. package/router/dist/benchmarks/command-benchmarks.d.ts +0 -143
  193. package/router/dist/benchmarks/command-benchmarks.js +0 -698
  194. package/router/dist/benchmarks/context-mode-normalized-benchmarks.d.ts +0 -82
  195. package/router/dist/benchmarks/context-mode-normalized-benchmarks.js +0 -773
  196. package/router/dist/benchmarks/context-mode-real-deep-benchmark.d.ts +0 -132
  197. package/router/dist/benchmarks/context-mode-real-deep-benchmark.js +0 -1380
  198. package/router/dist/benchmarks/context-saving-baseline.d.ts +0 -60
  199. package/router/dist/benchmarks/context-saving-baseline.js +0 -470
  200. package/router/dist/benchmarks/index-benchmarks.d.ts +0 -103
  201. package/router/dist/benchmarks/index-benchmarks.js +0 -862
  202. package/router/dist/benchmarks/storage-policy-benchmarks.d.ts +0 -101
  203. package/router/dist/benchmarks/storage-policy-benchmarks.js +0 -618
  204. package/router/dist/benchmarks/vault-index-benchmarks.d.ts +0 -56
  205. package/router/dist/benchmarks/vault-index-benchmarks.js +0 -322
  206. package/router/dist/config/config.d.ts +0 -103
  207. package/router/dist/config/config.js +0 -632
  208. package/router/dist/config/router-contract.d.ts +0 -10
  209. package/router/dist/config/router-contract.js +0 -54
  210. package/router/dist/config/schema.d.ts +0 -27
  211. package/router/dist/config/schema.js +0 -648
  212. package/router/dist/config/types.d.ts +0 -573
  213. package/router/dist/config/types.js +0 -41
  214. package/router/dist/evidence/bounded-evidence.d.ts +0 -40
  215. package/router/dist/evidence/bounded-evidence.js +0 -323
  216. package/router/dist/evidence/evidence-range-selector.d.ts +0 -24
  217. package/router/dist/evidence/evidence-range-selector.js +0 -232
  218. package/router/dist/evidence/evidence-search.d.ts +0 -27
  219. package/router/dist/evidence/evidence-search.js +0 -490
  220. package/router/dist/evidence/evidence.d.ts +0 -38
  221. package/router/dist/evidence/evidence.js +0 -200
  222. package/router/dist/evidence/failure-contracts.d.ts +0 -37
  223. package/router/dist/evidence/failure-contracts.js +0 -166
  224. package/router/dist/evidence/line-ranges.d.ts +0 -24
  225. package/router/dist/evidence/line-ranges.js +0 -22
  226. package/router/dist/experiments/local-index.d.ts +0 -64
  227. package/router/dist/experiments/local-index.js +0 -581
  228. package/router/dist/index.d.ts +0 -30
  229. package/router/dist/index.js +0 -32
  230. package/router/dist/local/local-traversal.d.ts +0 -18
  231. package/router/dist/local/local-traversal.js +0 -334
  232. package/router/dist/processing/engine.d.ts +0 -132
  233. package/router/dist/processing/engine.js +0 -533
  234. package/router/dist/processing/reducers.d.ts +0 -280
  235. package/router/dist/processing/reducers.js +0 -1673
  236. package/router/dist/processing/renderers.d.ts +0 -31
  237. package/router/dist/processing/renderers.js +0 -195
  238. package/router/dist/processing/scripts.d.ts +0 -91
  239. package/router/dist/processing/scripts.js +0 -386
  240. package/router/dist/repo/repo-traversal.d.ts +0 -18
  241. package/router/dist/repo/repo-traversal.js +0 -245
  242. package/router/dist/routing/observed-reducers.d.ts +0 -41
  243. package/router/dist/routing/observed-reducers.js +0 -661
  244. package/router/dist/routing/observed-routing.d.ts +0 -26
  245. package/router/dist/routing/observed-routing.js +0 -378
  246. package/router/dist/routing/parsers.d.ts +0 -16
  247. package/router/dist/routing/parsers.js +0 -521
  248. package/router/dist/routing/run-filters.d.ts +0 -60
  249. package/router/dist/routing/run-filters.js +0 -238
  250. package/router/dist/routing/run-reducers.d.ts +0 -49
  251. package/router/dist/routing/run-reducers.js +0 -122
  252. package/router/dist/sandbox/adapter-roots.d.ts +0 -46
  253. package/router/dist/sandbox/adapter-roots.js +0 -66
  254. package/router/dist/sandbox/eryx-python-adapter.d.ts +0 -13
  255. package/router/dist/sandbox/eryx-python-adapter.js +0 -868
  256. package/router/dist/sandbox/jq-wasm-adapter.d.ts +0 -9
  257. package/router/dist/sandbox/jq-wasm-adapter.js +0 -429
  258. package/router/dist/sandbox/quickjs-wasi-adapter.d.ts +0 -13
  259. package/router/dist/sandbox/quickjs-wasi-adapter.js +0 -384
  260. package/router/dist/sandbox/script-sandbox.d.ts +0 -134
  261. package/router/dist/sandbox/script-sandbox.js +0 -337
  262. package/router/dist/setup/script-transform-adapters.d.ts +0 -32
  263. package/router/dist/setup/script-transform-adapters.js +0 -278
  264. package/router/dist/tools/batch.d.ts +0 -33
  265. package/router/dist/tools/batch.js +0 -586
  266. package/router/dist/tools/run.d.ts +0 -62
  267. package/router/dist/tools/run.js +0 -1304
  268. package/router/dist/tools/search.d.ts +0 -66
  269. package/router/dist/tools/search.js +0 -1364
  270. package/router/dist/transform/engine.d.ts +0 -168
  271. package/router/dist/transform/engine.js +0 -2072
  272. package/router/dist/vault/vault-index.d.ts +0 -125
  273. package/router/dist/vault/vault-index.js +0 -416
  274. package/router/dist/vault/vault.d.ts +0 -138
  275. package/router/dist/vault/vault.js +0 -630
  276. package/runtime/interaction-contract.md +0 -16
  277. package/skills/discuss/references/checkpoints.md +0 -66
  278. package/skills/execute-work/references/execution-loop.md +0 -77
  279. package/skills/output-router/SKILL.md +0 -206
  280. package/skills/output-router/references/safety-policy.md +0 -40
  281. package/skills/setup-freeflow/references/output-router-setup.md +0 -110
  282. package/skills/track-work/references/working-record-schema.md +0 -378
  283. package/skills/workflow/references/workflow-loop.md +0 -92
  284. /package/skills/handoff/references/{templates.md → handoff-templates.md} +0 -0
  285. /package/skills/review-artifact/references/{reviewer-prompt.md → independent-artifact-reviewer-contract.md} +0 -0
@@ -1,1364 +0,0 @@
1
- import { createHash } from "node:crypto";
2
- import { readFile } from "node:fs/promises";
3
- import { resolve } from "node:path";
4
- import { buildBoundedEdgeChunks, buildBoundedExcerpt } from "../evidence/bounded-evidence.js";
5
- import { findBestLineForQuery, searchRepoEvidenceCandidates } from "../evidence/evidence-search.js";
6
- import { resolveExactLineRange } from "../evidence/line-ranges.js";
7
- import { collectLocalTextFileRefs, resolveLocalPath } from "../local/local-traversal.js";
8
- import { collectRepoTextFileRefs, resolveRepoPath } from "../repo/repo-traversal.js";
9
- import { createLocalVaultIndex } from "../vault/vault-index.js";
10
- import { createVault, readOutputText, readVaultRecord } from "../vault/vault.js";
11
- export const FREEFLOW_SEARCH_ACTIONS = ["query", "locate", "get", "retrieve", "expand", "explain", "transform"];
12
- export function searchActionForRetrievalAction(action) {
13
- return action;
14
- }
15
- const DEFAULT_CONTEXT_LINES = 2;
16
- const DEFAULT_QUERY_TOP_K = 1;
17
- const DEFAULT_LOCATE_TOP_K = 5;
18
- const MAX_TOP_K = 10;
19
- const QUERY_EXCERPT_MAX_BYTES = 8_192;
20
- const LINE_PREVIEW_MAX_BYTES = 2_048;
21
- const EXPAND_LINES_30_MAX_BYTES = 32 * 1024;
22
- const EXPAND_LINES_30_MAX_LINES = 120;
23
- const EXPAND_LINES_80_MAX_BYTES = 64 * 1024;
24
- const EXPAND_LINES_80_MAX_LINES = 240;
25
- const EXACT_LINE_RANGE_MAX_BYTES = 64_000;
26
- const EXACT_CHUNK_MAX_BYTES = 32_000;
27
- const CONCURRENT_REPO_FILE_READS = 32;
28
- const QUERY_COVERAGE_MAX_LINES = 80;
29
- const BOUNDED_EVIDENCE_CAPS = {
30
- queryExcerptMaxBytes: QUERY_EXCERPT_MAX_BYTES,
31
- linePreviewMaxBytes: LINE_PREVIEW_MAX_BYTES,
32
- expandLines30MaxBytes: EXPAND_LINES_30_MAX_BYTES,
33
- expandLines30MaxLines: EXPAND_LINES_30_MAX_LINES,
34
- expandLines80MaxBytes: EXPAND_LINES_80_MAX_BYTES,
35
- expandLines80MaxLines: EXPAND_LINES_80_MAX_LINES,
36
- exactChunkMaxBytes: EXACT_CHUNK_MAX_BYTES,
37
- };
38
- export async function freeflowSearch(options) {
39
- const preserve = options.preserve ?? "important";
40
- try {
41
- if (options.source.kind === "vault") {
42
- return await retrieveVault(options, preserve);
43
- }
44
- if (options.source.kind === "local") {
45
- if (options.action === "query") {
46
- return await queryFileSource(options.source.root, options, preserve);
47
- }
48
- if (options.action === "locate") {
49
- return await locateFileSource(options.source.root, options, preserve);
50
- }
51
- if (options.action === "get") {
52
- return await getFileSource(options.source.root, options, preserve);
53
- }
54
- if (options.action === "expand") {
55
- return await expandFileEvidence(options.source.root, options, preserve);
56
- }
57
- if (options.action === "retrieve") {
58
- return await retrieveFilePath(options.source.root, options, preserve);
59
- }
60
- if (options.action === "explain") {
61
- return explainFileDecision(options, preserve);
62
- }
63
- }
64
- const root = resolve(options.source.root);
65
- if (options.action === "query") {
66
- return await queryFileSource(root, options, preserve);
67
- }
68
- if (options.action === "locate") {
69
- return await locateFileSource(root, options, preserve);
70
- }
71
- if (options.action === "get") {
72
- return await getFileSource(root, options, preserve);
73
- }
74
- if (options.action === "expand") {
75
- return await expandFileEvidence(root, options, preserve);
76
- }
77
- if (options.action === "retrieve") {
78
- return await retrieveFilePath(root, options, preserve);
79
- }
80
- if (options.action === "explain") {
81
- return explainFileDecision(options, preserve);
82
- }
83
- return errorResult(preserve, `Retrieval action ${options.action} is not implemented yet.`);
84
- } catch (error) {
85
- const message = error instanceof Error ? error.message : String(error);
86
- return errorResult(preserve, `freeflow_search failed while reading ${options.source.kind} source: ${message}`);
87
- }
88
- }
89
- async function retrieveVault(options, preserve) {
90
- if (options.action === "retrieve") {
91
- if (!hasVaultOutputId(options)) {
92
- return errorResult(preserve, "Vault retrieve requires source.outputId.");
93
- }
94
- return retrieveVaultLines(options, preserve);
95
- }
96
- if (options.action === "expand") {
97
- if (!hasVaultOutputId(options)) {
98
- return errorResult(preserve, "Vault expansion requires source.outputId for exact raw recovery.");
99
- }
100
- return expandVaultEvidence(options, preserve);
101
- }
102
- if (options.action === "explain") {
103
- if (!hasVaultOutputId(options)) {
104
- return errorResult(preserve, "Vault explain requires source.outputId.");
105
- }
106
- return explainVaultOutput(options, preserve);
107
- }
108
- if (options.action !== "query" && options.action !== "locate" && options.action !== "get") {
109
- return errorResult(preserve, `Vault retrieval action ${options.action} is not implemented yet.`);
110
- }
111
- if (!options.query?.trim()) {
112
- return errorResult(preserve, `Vault ${options.action} requires a non-empty query string.`);
113
- }
114
- if (hasVaultOutputId(options) && (options.action === "query" || options.action === "get")) {
115
- return querySingleVaultOutput(options, preserve, options.action);
116
- }
117
- return queryVaultIndex(options, preserve, options.action);
118
- }
119
- async function querySingleVaultOutput(options, preserve, action = "query") {
120
- const query = options.query?.trim();
121
- if (!query) {
122
- return errorResult(preserve, "Vault query requires a non-empty query string.");
123
- }
124
- const vault = createVault({ root: options.source.root });
125
- const record = await readVaultRecord(vault, options.source.sessionId, options.source.outputId);
126
- const stream = resolveVaultStream(record, options.source.stream);
127
- const text = await readOutputText(vault, options.source.sessionId, options.source.outputId, stream);
128
- const lines = splitLines(text);
129
- const candidateLine = findBestLineForQuery(lines, query);
130
- if (candidateLine === null) {
131
- return {
132
- toolStatus: "ok",
133
- decisionId: decisionId(`vault-${action}`, options.source.outputId, stream, query, "none"),
134
- preserve,
135
- source: { kind: "vault", outputId: options.source.outputId, stream },
136
- ...vaultRecordResultFields(record),
137
- routing: {
138
- status: "routed",
139
- route: "retrieve",
140
- reason:
141
- action === "get"
142
- ? "Freeflow search get found no matching vaulted output evidence."
143
- : "Deterministic lexical vault retrieval found no matching output evidence.",
144
- },
145
- evidence: [],
146
- recovery: {
147
- how: `Refine the query or use freeflow_search action=retrieve with outputId=${options.source.outputId} and an exact line range.`,
148
- outputId: options.source.outputId,
149
- },
150
- };
151
- }
152
- const start = Math.max(0, candidateLine - DEFAULT_CONTEXT_LINES);
153
- const end = Math.min(lines.length - 1, candidateLine + DEFAULT_CONTEXT_LINES);
154
- const evidenceLines = `${start + 1}-${end + 1}`;
155
- const match = matchMetadataForText(lines[candidateLine] ?? "", query);
156
- const evidence = {
157
- id: evidenceIdFor(`${options.source.outputId}:${stream}`, evidenceLines, query),
158
- source: { kind: "vault", outputId: options.source.outputId, stream },
159
- path: `${options.source.outputId}:${stream}`,
160
- lines: evidenceLines,
161
- excerpt: excerptForLineRange(lines, { start: start + 1, end: end + 1 }, "small"),
162
- why:
163
- action === "get"
164
- ? `Best vaulted get match near line ${candidateLine + 1}; matchType=${match.type} confidence=${match.confidence.toFixed(2)}.`
165
- : `Deterministic lexical match for query terms in vaulted ${stream} output near line ${candidateLine + 1}.`,
166
- window: "small",
167
- expandable: true,
168
- ...(action === "get" ? { match } : {}),
169
- };
170
- return {
171
- toolStatus: "ok",
172
- decisionId: decisionId(`vault-${action}`, options.source.outputId, stream, query, evidenceLines),
173
- preserve,
174
- source: { kind: "vault", outputId: options.source.outputId, stream },
175
- ...vaultRecordResultFields(record),
176
- routing: {
177
- status: "routed",
178
- route: "retrieve",
179
- reason:
180
- action === "get"
181
- ? `Freeflow search get selected best vaulted outputId=${options.source.outputId} stream=${stream} lines=${evidenceLines}; matchType=${match.type} confidence=${match.confidence.toFixed(2)}.`
182
- : `Deterministic lexical retrieval selected vaulted outputId=${options.source.outputId} stream=${stream} lines=${evidenceLines}.`,
183
- },
184
- evidence: [evidence],
185
- recovery: {
186
- how:
187
- action === "get"
188
- ? `Use freeflow_search action=retrieve with outputId=${options.source.outputId}, stream=${stream}, and lineRange=${evidenceLines} for exact recovery; use action=expand with evidenceId=${evidence.id} for more context.`
189
- : `Use freeflow_search action=expand with evidenceId=${evidence.id}, or action=retrieve with outputId=${options.source.outputId} and stream=${stream}.`,
190
- outputId: options.source.outputId,
191
- evidenceId: evidence.id,
192
- },
193
- };
194
- }
195
- async function queryVaultIndex(options, preserve, action) {
196
- const query = options.query?.trim();
197
- if (!query) {
198
- return errorResult(preserve, `Vault ${action} requires a non-empty query string.`);
199
- }
200
- const topK = parseTopK(options.topK, action === "locate" ? DEFAULT_LOCATE_TOP_K : DEFAULT_QUERY_TOP_K);
201
- if (typeof topK === "string") {
202
- return errorResult(preserve, topK);
203
- }
204
- const vault = createVault({ root: options.source.root });
205
- const indexResult = await createLocalVaultIndex(vault).queryVault(
206
- query,
207
- vaultIndexFiltersFromSource(options.source, options.filters),
208
- {
209
- topK,
210
- maxExcerptBytes: action === "locate" ? LINE_PREVIEW_MAX_BYTES : QUERY_EXCERPT_MAX_BYTES,
211
- },
212
- );
213
- if (indexResult.matches.length === 0) {
214
- return {
215
- toolStatus: "ok",
216
- decisionId: decisionId(`vault-${action}-index`, query, String(topK), "none"),
217
- preserve,
218
- routing: {
219
- status: "routed",
220
- route: "retrieve",
221
- reason: `Vault-wide ${action} found no matching indexed evidence for the current filters.`,
222
- },
223
- evidence: [],
224
- recovery: {
225
- how: "Refine the query, adjust vault filters, or use source.outputId with action=retrieve/explain when the exact output is known.",
226
- },
227
- };
228
- }
229
- const evidence = indexResult.matches.map((match) => evidenceFromVaultIndexMatch(match, query, action));
230
- const first = indexResult.matches[0];
231
- const firstEvidence = evidence[0];
232
- if (!first || !firstEvidence) {
233
- return errorResult(preserve, `Vault ${action} produced no usable indexed evidence.`);
234
- }
235
- return {
236
- toolStatus: "ok",
237
- decisionId: decisionId(
238
- `vault-${action}-index`,
239
- query,
240
- String(topK),
241
- evidence.map((packet) => `${packet.path}:${packet.lines ?? ""}`).join("|"),
242
- ),
243
- preserve,
244
- source: sourceRefForVaultIndexMatch(first),
245
- routing: {
246
- status: "routed",
247
- route: "retrieve",
248
- reason:
249
- action === "locate"
250
- ? `Located ${evidence.length} indexed vault candidate(s) without raw output retrieval; top result ${first.outputId}${first.stream ? `:${first.stream}` : ""}${firstEvidence.lines ? `:${firstEvidence.lines}` : ""}.`
251
- : action === "get"
252
- ? `Freeflow search get selected best indexed vault match ${first.outputId}${first.stream ? `:${first.stream}` : ""}${firstEvidence.lines ? `:${firstEvidence.lines}` : ""}; matchType=${firstEvidence.match?.type ?? "lexical"} confidence=${(firstEvidence.match?.confidence ?? scoreConfidence(first.score)).toFixed(2)}.`
253
- : `Vault-wide query selected ${evidence.length} indexed candidate(s); top result ${first.outputId}${first.stream ? `:${first.stream}` : ""}${firstEvidence.lines ? `:${firstEvidence.lines}` : ""}.`,
254
- },
255
- evidence,
256
- recovery: {
257
- how: first.metadataOnly
258
- ? `Top match ${first.outputId} is metadata-only; use freeflow_search action=explain with outputId=${first.outputId} for recoverability details.`
259
- : `Use freeflow_search action=expand with source.outputId=${first.outputId} and evidenceId=${firstEvidence.id}, or action=retrieve with outputId=${first.outputId}${first.stream ? ` and stream=${first.stream}` : ""}.`,
260
- outputId: first.outputId,
261
- evidenceId: firstEvidence.id,
262
- },
263
- };
264
- }
265
- function evidenceFromVaultIndexMatch(match, query, action) {
266
- const source = sourceRefForVaultIndexMatch(match);
267
- const lines =
268
- match.lineStart !== undefined && match.lineEnd !== undefined ? `${match.lineStart}-${match.lineEnd}` : undefined;
269
- const path = `${match.outputId}${match.stream ? `:${match.stream}` : ":metadata"}`;
270
- const matchMetadata = match.metadataOnly
271
- ? { type: "metadata", confidence: scoreConfidence(match.score) }
272
- : matchMetadataForText(match.excerpt, query, match.score);
273
- const why = match.metadataOnly
274
- ? `Indexed metadata-only ${action} match for query terms; raw content is not recoverable from this entry.`
275
- : action === "get"
276
- ? `Best indexed vault get match; matchType=${matchMetadata.type} confidence=${matchMetadata.confidence.toFixed(2)} score=${match.score}.`
277
- : `Indexed vault ${action} match for query terms with score ${match.score}.`;
278
- return {
279
- id: evidenceIdFor(path, lines ?? match.chunkId, query),
280
- source,
281
- path,
282
- ...(lines !== undefined ? { lines } : {}),
283
- excerpt: match.excerpt,
284
- why,
285
- window: "small",
286
- expandable: !match.metadataOnly && lines !== undefined && match.stream !== undefined,
287
- ...(action === "get" ? { match: matchMetadata } : {}),
288
- };
289
- }
290
- function sourceRefForVaultIndexMatch(match) {
291
- return {
292
- kind: "vault",
293
- outputId: match.outputId,
294
- ...(match.stream !== undefined ? { stream: match.stream } : {}),
295
- };
296
- }
297
- function vaultIndexFiltersFromSource(source, filters) {
298
- return {
299
- ...filters,
300
- sessionId: filters?.sessionId ?? source.sessionId,
301
- ...(source.outputId !== undefined ? { outputId: source.outputId } : {}),
302
- ...(source.producerKind !== undefined ? { producerKind: source.producerKind } : {}),
303
- ...(source.server !== undefined ? { server: source.server } : {}),
304
- ...(source.tool !== undefined ? { tool: source.tool } : {}),
305
- ...(source.hostToolName !== undefined ? { hostToolName: source.hostToolName } : {}),
306
- ...(source.stream !== undefined ? { stream: source.stream } : {}),
307
- ...(source.recordKind !== undefined ? { recordKind: source.recordKind } : {}),
308
- ...(source.recoverability !== undefined ? { recoverability: source.recoverability } : {}),
309
- };
310
- }
311
- function hasVaultOutputId(options) {
312
- return typeof options.source.outputId === "string" && options.source.outputId.length > 0;
313
- }
314
- async function retrieveVaultLines(options, preserve) {
315
- const lineRange = options.lineRange;
316
- if (!lineRange) {
317
- return errorResult(preserve, "Vault retrieve requires a valid 1-based lineRange.");
318
- }
319
- const vault = createVault({ root: options.source.root });
320
- const record = await readVaultRecord(vault, options.source.sessionId, options.source.outputId);
321
- const stream = resolveVaultStream(record, options.source.stream);
322
- const text = await readOutputText(vault, options.source.sessionId, options.source.outputId, stream);
323
- const lines = splitLines(text);
324
- const resolvedRange = resolveExactLineRange({
325
- requested: lineRange,
326
- lineCount: lines.length,
327
- availableLabel: "available vaulted output lines",
328
- invalidReason: "Vault retrieve requires a valid 1-based lineRange.",
329
- });
330
- if (!resolvedRange.ok) {
331
- return errorResult(preserve, resolvedRange.reason);
332
- }
333
- const { start, end } = resolvedRange.range;
334
- const evidenceLines = `${start}-${end}`;
335
- const excerpt = lines.slice(start - 1, end).join("\n");
336
- if (byteLength(excerpt) > EXACT_LINE_RANGE_MAX_BYTES) {
337
- return retrieveVaultLineRangeOverCap(options, preserve, stream, record, lines, { start, end }, byteLength(excerpt));
338
- }
339
- const evidence = {
340
- id: evidenceIdFor(`${options.source.outputId}:${stream}`, evidenceLines, "retrieve"),
341
- source: { kind: "vault", outputId: options.source.outputId, stream },
342
- path: `${options.source.outputId}:${stream}`,
343
- lines: evidenceLines,
344
- excerpt,
345
- why: `Retrieved exact vaulted ${stream} output lines ${evidenceLines}.`,
346
- window: "exact",
347
- expandable: true,
348
- };
349
- return {
350
- toolStatus: "ok",
351
- decisionId: decisionId("vault-retrieve", options.source.outputId, stream, evidenceLines),
352
- preserve,
353
- source: { kind: "vault", outputId: options.source.outputId, stream },
354
- ...vaultRecordResultFields(record),
355
- routing: {
356
- status: "routed",
357
- route: "retrieve",
358
- reason: `Retrieved exact vaulted outputId=${options.source.outputId} stream=${stream} lines=${evidenceLines}.`,
359
- },
360
- evidence: [evidence],
361
- recovery: {
362
- how: `Use freeflow_search action=expand with evidenceId=${evidence.id} for more vaulted output context.`,
363
- outputId: options.source.outputId,
364
- evidenceId: evidence.id,
365
- },
366
- };
367
- }
368
- function retrieveVaultLineRangeOverCap(options, preserve, stream, record, lines, requestedRange, requestedBytes) {
369
- const evidence = buildBoundedEdgeChunks({ lines, range: requestedRange, caps: BOUNDED_EVIDENCE_CAPS }).map(
370
- (chunk, index) => {
371
- const evidenceLines = chunk.linesLabel;
372
- return {
373
- id: evidenceIdFor(`${options.source.outputId}:${stream}`, evidenceLines, `retrieve-over-cap-${index}`),
374
- source: { kind: "vault", outputId: options.source.outputId, stream },
375
- path: `${options.source.outputId}:${stream}`,
376
- lines: evidenceLines,
377
- excerpt: chunk.excerpt,
378
- why: `Bounded recoverable ${chunk.edge} preview for vaulted ${stream} output line range over cap ${EXACT_LINE_RANGE_MAX_BYTES}.`,
379
- window: "small",
380
- expandable: true,
381
- };
382
- },
383
- );
384
- return {
385
- toolStatus: "ok",
386
- decisionId: decisionId(
387
- "vault-retrieve-lines-over-cap",
388
- options.source.outputId,
389
- stream,
390
- `${requestedRange.start}-${requestedRange.end}`,
391
- String(requestedBytes),
392
- ),
393
- preserve,
394
- source: { kind: "vault", outputId: options.source.outputId, stream },
395
- ...vaultRecordResultFields(record),
396
- routing: {
397
- status: "partial",
398
- route: "retrieve",
399
- reason: `Requested vaulted outputId=${options.source.outputId} stream=${stream} lines=${requestedRange.start}-${requestedRange.end} is ${requestedBytes} bytes and exceeds cap ${EXACT_LINE_RANGE_MAX_BYTES}; returned bounded edge previews with recovery guidance.`,
400
- },
401
- evidence,
402
- recovery: {
403
- how: `Use narrower freeflow_search action=retrieve lineRange spans for exact vaulted output recovery, or native vault file access if a full raw span is required.`,
404
- outputId: options.source.outputId,
405
- },
406
- };
407
- }
408
- function expandVaultEvidenceOverCap(options, preserve, stream, record, evidence, lines, expandedRange, expandedBytes) {
409
- const chunks = buildBoundedEdgeChunks({ lines, range: expandedRange, caps: BOUNDED_EVIDENCE_CAPS }).map(
410
- (chunk, index) => {
411
- const evidenceLines = chunk.linesLabel;
412
- return {
413
- id: evidenceIdFor(`${options.source.outputId}:${stream}`, evidenceLines, `expand-over-cap-${index}`),
414
- source: { kind: "vault", outputId: options.source.outputId, stream },
415
- path: `${options.source.outputId}:${stream}`,
416
- lines: evidenceLines,
417
- excerpt: chunk.excerpt,
418
- why: `Bounded recoverable ${chunk.edge} preview for vaulted ${stream} expansion over cap ${EXACT_LINE_RANGE_MAX_BYTES}.`,
419
- window: "small",
420
- expandable: true,
421
- };
422
- },
423
- );
424
- const expandedLines = `${expandedRange.start}-${expandedRange.end}`;
425
- return {
426
- toolStatus: "ok",
427
- decisionId: decisionId("vault-expand-over-cap", evidence.id, expandedLines, String(expandedBytes)),
428
- preserve,
429
- source: { kind: "vault", outputId: options.source.outputId, stream },
430
- ...vaultRecordResultFields(record),
431
- routing: {
432
- status: "partial",
433
- route: "retrieve",
434
- reason: `Expanded vaulted outputId=${options.source.outputId} stream=${stream} evidence ${evidence.id} to lines=${expandedLines}, but ${expandedBytes} bytes exceeds cap ${EXACT_LINE_RANGE_MAX_BYTES}; returned bounded edge chunks with recovery guidance.`,
435
- },
436
- evidence: chunks,
437
- recovery: {
438
- how: `Use narrower freeflow_search action=retrieve lineRange spans for exact vaulted output recovery.`,
439
- outputId: options.source.outputId,
440
- evidenceId: evidence.id,
441
- },
442
- };
443
- }
444
- async function expandVaultEvidence(options, preserve) {
445
- const evidence = options.evidence;
446
- if (!evidence?.lines || evidence.source.kind !== "vault") {
447
- return errorResult(preserve, "Vault expansion requires a previous vault evidence packet with lines.");
448
- }
449
- const originalRange = parseLineRange(evidence.lines);
450
- if (!originalRange) {
451
- return errorResult(preserve, `Cannot expand unsupported vault evidence line range ${evidence.lines}.`);
452
- }
453
- const expansion = options.expansion ?? "lines_30";
454
- const vault = createVault({ root: options.source.root });
455
- const record = await readVaultRecord(vault, options.source.sessionId, options.source.outputId);
456
- const stream = resolveVaultStream(record, options.source.stream ?? evidence.source.stream);
457
- const text = await readOutputText(vault, options.source.sessionId, options.source.outputId, stream);
458
- const lines = splitLines(text);
459
- const expandedRange = expandLineRange(originalRange, lines.length, expansion);
460
- const window = windowForExpansion(expansion);
461
- const expandedExcerpt = lines.slice(expandedRange.start - 1, expandedRange.end).join("\n");
462
- if (expansion === "full" && byteLength(expandedExcerpt) > EXACT_LINE_RANGE_MAX_BYTES) {
463
- return expandVaultEvidenceOverCap(
464
- options,
465
- preserve,
466
- stream,
467
- record,
468
- evidence,
469
- lines,
470
- expandedRange,
471
- byteLength(expandedExcerpt),
472
- );
473
- }
474
- const bounded = buildBoundedExcerpt({ lines, range: expandedRange, window, caps: BOUNDED_EVIDENCE_CAPS });
475
- const expandedLines = bounded.linesLabel;
476
- const expandedEvidence = {
477
- ...evidence,
478
- source: { kind: "vault", outputId: options.source.outputId, stream },
479
- path: `${options.source.outputId}:${stream}`,
480
- lines: expandedLines,
481
- excerpt: bounded.excerpt,
482
- why: bounded.truncatedByLineCap
483
- ? `Expanded deterministic vault evidence from ${evidence.lines} and bounded it to ${expandedLines} by the ${window} line cap.`
484
- : `Expanded deterministic vault evidence from ${evidence.lines} to ${expandedLines}.`,
485
- window,
486
- expandable: expansion !== "full",
487
- };
488
- return {
489
- toolStatus: "ok",
490
- decisionId: decisionId("vault-expand", evidence.id, expandedLines),
491
- preserve,
492
- source: { kind: "vault", outputId: options.source.outputId, stream },
493
- ...vaultRecordResultFields(record),
494
- routing: {
495
- status: "routed",
496
- route: "retrieve",
497
- reason: `Expanded vaulted outputId=${options.source.outputId} stream=${stream} evidence ${evidence.id} to lines=${expandedLines}.`,
498
- },
499
- evidence: [expandedEvidence],
500
- recovery: {
501
- how: `Use freeflow_search action=retrieve with outputId=${options.source.outputId}, stream=${stream}, and an exact line range for precise recovery.`,
502
- outputId: options.source.outputId,
503
- evidenceId: evidence.id,
504
- },
505
- };
506
- }
507
- async function explainVaultOutput(options, preserve) {
508
- const vault = createVault({ root: options.source.root });
509
- const record = await readVaultRecord(vault, options.source.sessionId, options.source.outputId);
510
- const producer = producerForVaultRecord(record);
511
- const persistence = persistenceForVaultRecord(record);
512
- const details = vaultRecordDetails(record);
513
- const recovery = vaultRecordRecovery(record, options.source.stream);
514
- const source = { kind: "vault", outputId: options.source.outputId };
515
- if (options.source.stream !== undefined) {
516
- Object.assign(source, { stream: options.source.stream });
517
- }
518
- return {
519
- toolStatus: "ok",
520
- decisionId: decisionId("vault-explain", options.source.outputId, record.contentHashSha256),
521
- preserve,
522
- source,
523
- ...vaultRecordResultFields(record),
524
- routing: {
525
- status: "routed",
526
- route: "retrieve",
527
- reason: `Vault outputId=${options.source.outputId} ${details}; producer=${producer.kind}; persistence.status=${persistence.status}; recoverability=${persistence.recoverability}. ${recovery.reason}`,
528
- },
529
- evidence: [],
530
- recovery: recovery.hint,
531
- };
532
- }
533
- function resolveVaultStream(record, requested) {
534
- if (requested !== undefined) {
535
- return requested;
536
- }
537
- if (record.kind === "command") {
538
- return "combined";
539
- }
540
- if (record.kind === "text") {
541
- return "raw";
542
- }
543
- if (record.kind === "metadata") {
544
- throw new Error("Metadata only records store no raw stream.");
545
- }
546
- throw new Error("Repo file reference records store metadata only and have no raw stream.");
547
- }
548
- function vaultRecordResultFields(record) {
549
- const fields = {
550
- producer: producerForVaultRecord(record),
551
- persistence: persistenceForVaultRecord(record),
552
- };
553
- const recordId = record.recordId;
554
- if (typeof recordId === "string") {
555
- fields.recordId = recordId;
556
- }
557
- if (record.lineage !== undefined) {
558
- fields.lineage = record.lineage;
559
- }
560
- return fields;
561
- }
562
- function producerForVaultRecord(record) {
563
- const producer = record.producer;
564
- if (producer?.kind) {
565
- return producer;
566
- }
567
- if (record.kind === "command") {
568
- return { kind: "command" };
569
- }
570
- if (record.kind === "text" || record.kind === "metadata") {
571
- if (record.sourceKind === "native") {
572
- return { kind: "native" };
573
- }
574
- if (record.sourceKind === "mcp") {
575
- return { kind: "mcp" };
576
- }
577
- if (record.sourceKind === "web") {
578
- return { kind: "web" };
579
- }
580
- if (record.sourceKind === "fetch") {
581
- return { kind: "fetch" };
582
- }
583
- if (record.sourceKind === "code_search") {
584
- return { kind: "code_search" };
585
- }
586
- return { kind: "other" };
587
- }
588
- return { kind: "repo" };
589
- }
590
- function persistenceForVaultRecord(record) {
591
- const persistence = record.persistence;
592
- if (persistence?.status && persistence.recoverability) {
593
- return persistence;
594
- }
595
- if (record.kind === "repo-file" || record.kind === "metadata") {
596
- return { status: "metadata_only", recoverability: "metadata_only", outputId: record.outputId };
597
- }
598
- return {
599
- status: "vaulted",
600
- recoverability: "exact",
601
- recoveryOutputId: record.outputId,
602
- outputId: record.outputId,
603
- };
604
- }
605
- function vaultRecordDetails(record) {
606
- const decisions = Array.isArray(record.decisionIds) ? record.decisionIds.join(",") : "";
607
- const recordId = record.recordId;
608
- const recordIdText = typeof recordId === "string" ? ` recordId=${recordId}` : "";
609
- if (record.kind === "command") {
610
- return `kind=command${recordIdText} executionStatus=${record.executionStatus} exitCode=${record.exitCode} decisions=${decisions}`;
611
- }
612
- if (record.kind === "text") {
613
- return `kind=text${recordIdText} sourceKind=${record.sourceKind} decisions=${decisions}`;
614
- }
615
- if (record.kind === "metadata") {
616
- return `kind=metadata${recordIdText} sourceKind=${record.sourceKind} rawLines=${record.lineCounts.raw} rawBytes=${record.byteCounts.raw} decisions=${decisions}`;
617
- }
618
- return `kind=repo-file${recordIdText} path=${record.path} decisions=${decisions}`;
619
- }
620
- function vaultRecordRecovery(record, requestedStream) {
621
- const persistence = persistenceForVaultRecord(record);
622
- const recoveryOutputId = persistence.recoveryOutputId ?? persistence.outputId ?? record.outputId;
623
- if (persistence.recoverability === "exact") {
624
- const stream = requestedStream ?? resolveVaultStream(record);
625
- const streamText = record.kind === "command" && requestedStream === undefined ? "stdout|stderr|combined" : stream;
626
- return {
627
- reason: `Exact content is recoverable from the vault with recoveryOutputId=${recoveryOutputId}.`,
628
- hint: {
629
- how: `Use freeflow_search action=retrieve with outputId=${recoveryOutputId}, stream=${streamText}, and an exact lineRange to recover exact vaulted content.`,
630
- outputId: recoveryOutputId,
631
- },
632
- };
633
- }
634
- if (persistence.recoverability === "redacted") {
635
- return {
636
- reason: `Only redacted content is recoverable from the vault; exact raw content is intentionally unavailable.`,
637
- hint: {
638
- how: `Use freeflow_search action=retrieve with outputId=${recoveryOutputId} to recover redacted persisted content. Exact raw recovery is unavailable.`,
639
- outputId: recoveryOutputId,
640
- },
641
- };
642
- }
643
- if (persistence.recoverability === "metadata_only") {
644
- return {
645
- reason: "Only metadata was persisted; no raw content recovery is promised.",
646
- hint: {
647
- how: "Only vault metadata is available for this record. There is no raw content stream to recover.",
648
- },
649
- };
650
- }
651
- return {
652
- reason: "No content was persisted; recovery is unavailable.",
653
- hint: {
654
- how: "No persisted content is available for this record.",
655
- },
656
- };
657
- }
658
- async function queryFileSource(root, options, preserve) {
659
- const sourceKind = options.source.kind;
660
- const sourceLabel = sourceKindLabel(sourceKind);
661
- if (!options.query?.trim()) {
662
- return errorResult(preserve, `${sourceLabel} query requires a non-empty query string.`);
663
- }
664
- const query = options.query.trim();
665
- const topK = parseTopK(options.topK, DEFAULT_QUERY_TOP_K);
666
- if (typeof topK === "string") {
667
- return errorResult(preserve, topK);
668
- }
669
- const files = await readSearchTextFiles(sourceKind, root, options.source.path, options.generatedPathGlobs);
670
- const candidates = searchRepoEvidenceCandidates({
671
- files,
672
- query,
673
- topK,
674
- defaultContextLines: DEFAULT_CONTEXT_LINES,
675
- queryCoverageMaxLines: QUERY_COVERAGE_MAX_LINES,
676
- });
677
- if (candidates.length === 0) {
678
- return {
679
- toolStatus: "ok",
680
- decisionId: decisionId(`${sourceKind}-query`, query, root, "none"),
681
- preserve,
682
- source: sourceRefForOptions(options.source, options.source.path ?? "."),
683
- routing: {
684
- status: "routed",
685
- route: "retrieve",
686
- reason: `Deterministic lexical retrieval found no matching ${sourceKind} evidence.`,
687
- },
688
- evidence: [],
689
- recovery: {
690
- how: "Refine the query, use action=locate for candidate paths, or use native read for a known whole file.",
691
- },
692
- };
693
- }
694
- const evidence = candidates.map((candidate) => evidenceFromCandidate(candidate, query));
695
- const first = evidence[0];
696
- const firstCandidate = candidates[0];
697
- if (!first || !firstCandidate) {
698
- return errorResult(preserve, `${sourceLabel} query produced no usable candidate evidence.`);
699
- }
700
- return {
701
- toolStatus: "ok",
702
- decisionId: decisionId(
703
- `${sourceKind}-query`,
704
- query,
705
- String(topK),
706
- evidence.map((packet) => `${packet.path}:${packet.lines ?? ""}`).join("|"),
707
- ),
708
- preserve,
709
- source: fileSourceRef(firstCandidate.file),
710
- routing: {
711
- status: "routed",
712
- route: "retrieve",
713
- reason: `Deterministic ${sourceKind} retrieval selected ${evidence.length} candidate(s); top result ${firstCandidate.file.path}:${first.lines} (${firstCandidate.reason}).`,
714
- },
715
- evidence,
716
- recovery: {
717
- how: recoveryHowForFile(
718
- firstCandidate.file,
719
- `Use freeflow_search action=expand with evidenceId=${first.id} for more surrounding context, action=locate for candidate paths, or action=retrieve`,
720
- ),
721
- evidenceId: first.id,
722
- },
723
- };
724
- }
725
- async function getFileSource(root, options, preserve) {
726
- const sourceKind = options.source.kind;
727
- const sourceLabel = sourceKindLabel(sourceKind);
728
- if (!options.query?.trim()) {
729
- return errorResult(preserve, `${sourceLabel} get requires a non-empty query string.`);
730
- }
731
- const query = options.query.trim();
732
- const files = await readSearchTextFiles(sourceKind, root, options.source.path, options.generatedPathGlobs);
733
- const candidates = searchRepoEvidenceCandidates({
734
- files,
735
- query,
736
- topK: 1,
737
- defaultContextLines: DEFAULT_CONTEXT_LINES,
738
- queryCoverageMaxLines: QUERY_COVERAGE_MAX_LINES,
739
- });
740
- const candidate = candidates[0];
741
- if (!candidate) {
742
- return {
743
- toolStatus: "ok",
744
- decisionId: decisionId(`${sourceKind}-get`, query, root, "none"),
745
- preserve,
746
- source: sourceRefForOptions(options.source, options.source.path ?? "."),
747
- routing: {
748
- status: "routed",
749
- route: "retrieve",
750
- reason: `Freeflow search get found no matching ${sourceKind} evidence.`,
751
- },
752
- evidence: [],
753
- recovery: {
754
- how: "Refine the query, use action=locate for candidate paths, or use native read for a known whole file.",
755
- },
756
- };
757
- }
758
- const match = matchMetadataForCandidate(candidate);
759
- const evidence = evidenceFromCandidate(candidate, query);
760
- evidence.why = `Best ${sourceKind} get match; matchType=${match.type} confidence=${match.confidence.toFixed(2)}. ${evidence.why}`;
761
- evidence.match = match;
762
- return {
763
- toolStatus: "ok",
764
- decisionId: decisionId(`${sourceKind}-get`, query, evidence.path ?? candidate.file.path, evidence.lines ?? ""),
765
- preserve,
766
- source: fileSourceRef(candidate.file),
767
- routing: {
768
- status: "routed",
769
- route: "retrieve",
770
- reason: `Freeflow search get selected best ${sourceKind} match ${candidate.file.path}:${evidence.lines}; matchType=${match.type} confidence=${match.confidence.toFixed(2)} (${candidate.reason}).`,
771
- },
772
- evidence: [evidence],
773
- recovery: {
774
- how: recoveryHowForFile(
775
- candidate.file,
776
- `Use freeflow_search action=retrieve with lineRange=${evidence.lines} for exact recovery, or action=expand with evidenceId=${evidence.id}`,
777
- ),
778
- evidenceId: evidence.id,
779
- },
780
- };
781
- }
782
- async function locateFileSource(root, options, preserve) {
783
- const sourceKind = options.source.kind;
784
- const sourceLabel = sourceKindLabel(sourceKind);
785
- if (!options.query?.trim()) {
786
- return errorResult(preserve, `${sourceLabel} locate requires a non-empty query string.`);
787
- }
788
- const query = options.query.trim();
789
- const topK = parseTopK(options.topK, DEFAULT_LOCATE_TOP_K);
790
- if (typeof topK === "string") {
791
- return errorResult(preserve, topK);
792
- }
793
- const files = await readSearchTextFiles(sourceKind, root, options.source.path, options.generatedPathGlobs);
794
- const candidates = searchRepoEvidenceCandidates({
795
- files,
796
- query,
797
- topK,
798
- defaultContextLines: DEFAULT_CONTEXT_LINES,
799
- queryCoverageMaxLines: QUERY_COVERAGE_MAX_LINES,
800
- });
801
- if (candidates.length === 0) {
802
- return {
803
- toolStatus: "ok",
804
- decisionId: decisionId(`${sourceKind}-locate`, query, root, "none"),
805
- preserve,
806
- source: sourceRefForOptions(options.source, options.source.path ?? "."),
807
- routing: {
808
- status: "routed",
809
- route: "retrieve",
810
- reason: "Deterministic lexical locate found no candidate location.",
811
- },
812
- evidence: [],
813
- recovery: {
814
- how: "Refine the query or use native read for a known whole file.",
815
- },
816
- };
817
- }
818
- const evidence = candidates.map((candidate) => {
819
- const line = candidate.lineIndex + 1;
820
- return evidenceFromRange({
821
- id: evidenceIdFor(fileEvidenceKey(candidate.file), `${line}-${line}`, query),
822
- file: candidate.file,
823
- lines: { start: line, end: line },
824
- why: `Candidate location from deterministic ${sourceKind} scoring.`,
825
- window: "small",
826
- expandable: true,
827
- });
828
- });
829
- const first = evidence[0];
830
- const firstCandidate = candidates[0];
831
- if (!first || !firstCandidate) {
832
- return errorResult(preserve, `${sourceLabel} locate produced no usable candidate evidence.`);
833
- }
834
- return {
835
- toolStatus: "ok",
836
- decisionId: decisionId(
837
- `${sourceKind}-locate`,
838
- query,
839
- String(topK),
840
- evidence.map((packet) => `${packet.path}:${packet.lines ?? ""}`).join("|"),
841
- ),
842
- preserve,
843
- source: fileSourceRef(firstCandidate.file),
844
- routing: {
845
- status: "routed",
846
- route: "retrieve",
847
- reason: `Located ${evidence.length} candidate location(s) without broad evidence retrieval; top result ${firstCandidate.file.path}:${first.lines} (${firstCandidate.reason}).`,
848
- },
849
- evidence,
850
- recovery: {
851
- how: recoveryHowForFile(
852
- firstCandidate.file,
853
- `Use freeflow_search action=retrieve for an explicit span, or action=expand with evidenceId=${first.id}`,
854
- ),
855
- evidenceId: first.id,
856
- },
857
- };
858
- }
859
- async function expandFileEvidence(root, options, preserve) {
860
- const sourceKind = options.source.kind;
861
- const sourceLabel = sourceKindLabel(sourceKind);
862
- const evidence = options.evidence;
863
- if (!evidence?.path || !evidence.lines) {
864
- return errorResult(
865
- preserve,
866
- `${sourceLabel} expansion requires a previous ${sourceKind} evidence packet with path and lines.`,
867
- );
868
- }
869
- const expansion = options.expansion ?? "lines_30";
870
- const originalRange = parseLineRange(evidence.lines);
871
- if (!originalRange) {
872
- return errorResult(preserve, `Cannot expand unsupported evidence line range ${evidence.lines}.`);
873
- }
874
- const file = await readSearchTextFile(sourceKind, root, evidence.path);
875
- const expandedRange = expandLineRange(originalRange, file.lines.length, expansion);
876
- const expandedLines = `${expandedRange.start}-${expandedRange.end}`;
877
- const expandedExcerpt = file.lines.slice(expandedRange.start - 1, expandedRange.end).join("\n");
878
- if (expansion === "full" && byteLength(expandedExcerpt) > EXACT_LINE_RANGE_MAX_BYTES) {
879
- return expandFileEvidenceOverCap(file, evidence, expandedRange, preserve, byteLength(expandedExcerpt));
880
- }
881
- const expandedEvidence = evidenceFromRange({
882
- id: evidence.id,
883
- file,
884
- lines: expandedRange,
885
- why: `Expanded deterministic ${sourceKind} evidence from ${evidence.lines} to ${expandedLines}.`,
886
- window: windowForExpansion(expansion),
887
- expandable: expansion !== "full",
888
- });
889
- return {
890
- toolStatus: "ok",
891
- decisionId: decisionId(`${sourceKind}-expand`, evidence.id, expandedEvidence.lines ?? expandedLines),
892
- preserve,
893
- source: fileSourceRef(file),
894
- routing: {
895
- status: "routed",
896
- route: "retrieve",
897
- reason: `Expanded ${sourceKind} evidence ${evidence.id} to ${evidence.path}:${expandedEvidence.lines ?? expandedLines}.`,
898
- },
899
- evidence: [expandedEvidence],
900
- recovery: {
901
- how:
902
- expansion === "full"
903
- ? `Use native read for direct whole-file behavior if needed for ${file.path}.`
904
- : `Use freeflow_search action=expand with expansion=lines_80 or full for more context from ${file.path}.`,
905
- evidenceId: evidence.id,
906
- },
907
- };
908
- }
909
- async function retrieveFilePath(root, options, preserve) {
910
- const sourceKind = options.source.kind;
911
- const sourceLabel = sourceKindLabel(sourceKind);
912
- if (!options.source.path) {
913
- return errorResult(preserve, `${sourceLabel} retrieve requires source.path.`);
914
- }
915
- const file = await readSearchTextFile(sourceKind, root, options.source.path);
916
- const maxFullBytes = options.maxFullBytes ?? 64_000;
917
- if (options.lineRange) {
918
- return retrieveFileLineRange(file, options.lineRange, preserve);
919
- }
920
- if (preserve === "full") {
921
- return retrieveFullFile(file, maxFullBytes);
922
- }
923
- const end = Math.min(file.lines.length, 10);
924
- const evidence = evidenceFromRange({
925
- id: evidenceIdFor(fileEvidenceKey(file), `1-${end}`, "retrieve"),
926
- file,
927
- lines: { start: 1, end },
928
- why: `Retrieved explicit ${file.sourceKind} span ${file.path}:1-${end}.`,
929
- window: "small",
930
- expandable: true,
931
- });
932
- return {
933
- toolStatus: "ok",
934
- decisionId: decisionId(`${file.sourceKind}-retrieve`, file.path, evidence.lines ?? ""),
935
- preserve,
936
- source: fileSourceRef(file),
937
- routing: {
938
- status: "routed",
939
- route: "retrieve",
940
- reason: `Retrieved explicit ${file.sourceKind} span ${file.path}:${evidence.lines}.`,
941
- },
942
- evidence: [evidence],
943
- recovery: {
944
- how: `Use freeflow_search action=expand with evidenceId=${evidence.id} for more context from ${file.path}.`,
945
- evidenceId: evidence.id,
946
- },
947
- };
948
- }
949
- function expandFileEvidenceOverCap(file, evidence, expandedRange, preserve, expandedBytes) {
950
- const chunks = buildBoundedEdgeChunks({ lines: file.lines, range: expandedRange, caps: BOUNDED_EVIDENCE_CAPS }).map(
951
- (chunk, index) => fileEvidenceForBoundedExactChunk(file, chunk, index),
952
- );
953
- const expandedLines = `${expandedRange.start}-${expandedRange.end}`;
954
- return {
955
- toolStatus: "ok",
956
- decisionId: decisionId(`${file.sourceKind}-expand-over-cap`, evidence.id, expandedLines, String(expandedBytes)),
957
- preserve,
958
- source: fileSourceRef(file),
959
- routing: {
960
- status: "partial",
961
- route: "retrieve",
962
- reason: `Expanded ${file.sourceKind} evidence ${evidence.id} to ${file.path}:${expandedLines}, but ${expandedBytes} bytes exceeds cap ${EXACT_LINE_RANGE_MAX_BYTES}; returned bounded edge chunks with recovery guidance.`,
963
- },
964
- evidence: chunks,
965
- recovery: {
966
- how: `Use narrower freeflow_search action=retrieve lineRange spans for exact content recovery from ${file.path}, or native read for direct whole-file host behavior.`,
967
- evidenceId: evidence.id,
968
- },
969
- };
970
- }
971
- function retrieveFileLineRange(file, lineRange, preserve) {
972
- const resolvedRange = resolveExactLineRange({
973
- requested: lineRange,
974
- lineCount: file.lines.length,
975
- availableLabel: `available ${file.sourceKind} lines`,
976
- invalidReason: `${sourceKindLabel(file.sourceKind)} retrieve requires a valid 1-based lineRange.`,
977
- });
978
- if (!resolvedRange.ok) {
979
- return errorResult(preserve, resolvedRange.reason);
980
- }
981
- const { start, end } = resolvedRange.range;
982
- const evidenceLines = `${start}-${end}`;
983
- const excerpt = file.lines.slice(start - 1, end).join("\n");
984
- if (byteLength(excerpt) > EXACT_LINE_RANGE_MAX_BYTES) {
985
- return retrieveFileLineRangeOverCap(file, { start, end }, preserve, byteLength(excerpt));
986
- }
987
- const evidence = evidenceFromRange({
988
- id: evidenceIdFor(fileEvidenceKey(file), evidenceLines, "retrieve"),
989
- file,
990
- lines: { start, end },
991
- why: `Retrieved exact ${file.sourceKind} lines ${file.path}:${evidenceLines}.`,
992
- window: "exact",
993
- expandable: true,
994
- });
995
- return {
996
- toolStatus: "ok",
997
- decisionId: decisionId(`${file.sourceKind}-retrieve-lines`, file.path, evidenceLines),
998
- preserve,
999
- source: fileSourceRef(file),
1000
- routing: {
1001
- status: "routed",
1002
- route: "retrieve",
1003
- reason: `Retrieved exact ${file.sourceKind} lines ${file.path}:${evidenceLines}.`,
1004
- },
1005
- evidence: [evidence],
1006
- recovery: {
1007
- how: `Use freeflow_search action=expand with evidenceId=${evidence.id} for more context from ${file.path}.`,
1008
- evidenceId: evidence.id,
1009
- },
1010
- };
1011
- }
1012
- function retrieveFileLineRangeOverCap(file, requestedRange, preserve, requestedBytes) {
1013
- const evidence = buildBoundedEdgeChunks({
1014
- lines: file.lines,
1015
- range: requestedRange,
1016
- caps: BOUNDED_EVIDENCE_CAPS,
1017
- }).map((chunk, index) => fileEvidenceForBoundedExactChunk(file, chunk, index));
1018
- return {
1019
- toolStatus: "ok",
1020
- decisionId: decisionId(
1021
- `${file.sourceKind}-retrieve-lines-over-cap`,
1022
- file.path,
1023
- `${requestedRange.start}-${requestedRange.end}`,
1024
- String(requestedBytes),
1025
- ),
1026
- preserve,
1027
- source: fileSourceRef(file),
1028
- routing: {
1029
- status: "partial",
1030
- route: "retrieve",
1031
- reason: `Requested ${file.sourceKind} lines ${file.path}:${requestedRange.start}-${requestedRange.end} are ${requestedBytes} bytes and exceed cap ${EXACT_LINE_RANGE_MAX_BYTES}; returned bounded edge previews with recovery guidance.`,
1032
- },
1033
- evidence,
1034
- recovery: {
1035
- how: `Use narrower freeflow_search action=retrieve lineRange spans for exact content recovery from ${file.path}, or native read for direct whole-file host behavior.`,
1036
- },
1037
- };
1038
- }
1039
- function fileEvidenceForBoundedExactChunk(file, chunk, index) {
1040
- const evidenceLines = chunk.linesLabel;
1041
- return {
1042
- id: evidenceIdFor(fileEvidenceKey(file), evidenceLines, `retrieve-over-cap-${index}`),
1043
- source: fileSourceRef(file),
1044
- path: file.path,
1045
- lines: evidenceLines,
1046
- excerpt: chunk.excerpt,
1047
- why: `Bounded recoverable ${chunk.edge} preview for ${file.sourceKind} line range over cap ${EXACT_LINE_RANGE_MAX_BYTES}.`,
1048
- window: "small",
1049
- expandable: true,
1050
- };
1051
- }
1052
- function retrieveFullFile(file, maxFullBytes) {
1053
- const fullBytes = byteLength(file.text);
1054
- if (fullBytes <= maxFullBytes) {
1055
- const evidence = evidenceFromRange({
1056
- id: evidenceIdFor(fileEvidenceKey(file), `1-${file.lines.length}`, "full"),
1057
- file,
1058
- lines: { start: 1, end: file.lines.length },
1059
- why: `Returned full exact content because ${file.path} is under the full-context cap.`,
1060
- window: "full",
1061
- expandable: false,
1062
- });
1063
- return {
1064
- toolStatus: "ok",
1065
- decisionId: decisionId(`${file.sourceKind}-full`, file.path, String(fullBytes)),
1066
- preserve: "full",
1067
- source: fileSourceRef(file),
1068
- routing: {
1069
- status: "routed",
1070
- route: "retrieve",
1071
- reason: `Returned full exact content for ${file.path}; ${fullBytes} bytes is under cap ${maxFullBytes}.`,
1072
- },
1073
- evidence: [evidence],
1074
- recovery: {
1075
- how: `Use native read for direct whole-file host behavior if needed for ${file.path}.`,
1076
- evidenceId: evidence.id,
1077
- },
1078
- };
1079
- }
1080
- const evidence = buildBoundedEdgeChunks({
1081
- lines: file.lines,
1082
- range: { start: 1, end: file.lines.length },
1083
- caps: BOUNDED_EVIDENCE_CAPS,
1084
- }).map((chunk, index) => fileEvidenceForBoundedExactChunk(file, chunk, index));
1085
- return {
1086
- toolStatus: "ok",
1087
- decisionId: decisionId(`${file.sourceKind}-full-over-cap`, file.path, String(fullBytes), String(maxFullBytes)),
1088
- preserve: "full",
1089
- source: fileSourceRef(file),
1090
- routing: {
1091
- status: "partial",
1092
- route: "retrieve",
1093
- reason: `Full exact content for ${file.path} is ${fullBytes} bytes and exceeds cap ${maxFullBytes}; returned bounded edge previews instead of a summary.`,
1094
- },
1095
- evidence,
1096
- recovery: {
1097
- how: recoveryHowForFile(
1098
- file,
1099
- "Use freeflow_search action=retrieve with an explicit span to recover exact content, or native read for direct whole-file output",
1100
- ),
1101
- },
1102
- };
1103
- }
1104
- function explainFileDecision(options, preserve) {
1105
- const decision = options.decision;
1106
- if (!decision) {
1107
- return errorResult(preserve, "File-source explain requires a previous routed result decision.");
1108
- }
1109
- const recovery = {
1110
- how:
1111
- decision.recovery?.how ??
1112
- "Use freeflow_search action=expand with a returned evidence id, or action=retrieve for an explicit file-source span.",
1113
- };
1114
- if (decision.recovery?.outputId !== undefined) {
1115
- Object.assign(recovery, { outputId: decision.recovery.outputId });
1116
- }
1117
- if (decision.recovery?.evidenceId !== undefined) {
1118
- Object.assign(recovery, { evidenceId: decision.recovery.evidenceId });
1119
- }
1120
- const result = {
1121
- toolStatus: "ok",
1122
- decisionId: decisionId("file-explain", decision.decisionId),
1123
- preserve: decision.preserve,
1124
- routing: {
1125
- status: "routed",
1126
- route: "retrieve",
1127
- reason: `Decision ${decision.decisionId} used route=${decision.routing.route}, routing.status=${decision.routing.status}: ${decision.routing.reason}`,
1128
- },
1129
- evidence: [],
1130
- recovery,
1131
- };
1132
- if (decision.source !== undefined) {
1133
- result.source = decision.source;
1134
- }
1135
- return result;
1136
- }
1137
- async function readSearchTextFiles(sourceKind, root, requestedPath, generatedPathGlobs = []) {
1138
- if (sourceKind === "local") {
1139
- const options = {
1140
- root,
1141
- generatedPathGlobs,
1142
- };
1143
- if (requestedPath !== undefined) {
1144
- options.requestedPath = requestedPath;
1145
- }
1146
- const fileRefs = await collectLocalTextFileRefs(options);
1147
- return readSearchTextFileRefs("local", fileRefs);
1148
- }
1149
- const options = {
1150
- root,
1151
- generatedPathGlobs,
1152
- };
1153
- if (requestedPath !== undefined) {
1154
- options.requestedPath = requestedPath;
1155
- }
1156
- const fileRefs = await collectRepoTextFileRefs(options);
1157
- return readSearchTextFileRefs("repo", fileRefs);
1158
- }
1159
- async function readSearchTextFile(sourceKind, root, path) {
1160
- const resolved = sourceKind === "local" ? await resolveLocalPath(root, path) : await resolveRepoPath(root, path);
1161
- const text = await readFile(resolved.absolutePath, "utf8");
1162
- return {
1163
- sourceKind,
1164
- root: resolved.root,
1165
- path: resolved.relativePath,
1166
- absolutePath: resolved.absolutePath,
1167
- text,
1168
- lines: splitLines(text),
1169
- };
1170
- }
1171
- async function readSearchTextFileRefs(sourceKind, fileRefs) {
1172
- const files = new Array(fileRefs.length);
1173
- let nextIndex = 0;
1174
- async function worker() {
1175
- while (nextIndex < fileRefs.length) {
1176
- const currentIndex = nextIndex;
1177
- nextIndex += 1;
1178
- const ref = fileRefs[currentIndex];
1179
- if (!ref) {
1180
- continue;
1181
- }
1182
- const text = await readFile(ref.absolutePath, "utf8");
1183
- if (text.includes("\0")) {
1184
- continue;
1185
- }
1186
- files[currentIndex] = {
1187
- sourceKind,
1188
- root: "root" in ref ? ref.root : "",
1189
- path: ref.path,
1190
- absolutePath: ref.absolutePath,
1191
- text,
1192
- lines: splitLines(text),
1193
- };
1194
- }
1195
- }
1196
- const workerCount = Math.min(CONCURRENT_REPO_FILE_READS, fileRefs.length);
1197
- await Promise.all(Array.from({ length: workerCount }, () => worker()));
1198
- return files.filter((file) => Boolean(file));
1199
- }
1200
- function sourceKindLabel(sourceKind) {
1201
- return sourceKind === "local" ? "Local" : "Repo";
1202
- }
1203
- function sourceRefForOptions(source, path) {
1204
- if (source.kind === "local") {
1205
- return { kind: "local", root: source.root, path };
1206
- }
1207
- return { kind: "repo", path };
1208
- }
1209
- function fileSourceRef(file) {
1210
- if (file.sourceKind === "local") {
1211
- return { kind: "local", root: file.root, path: file.path };
1212
- }
1213
- return { kind: "repo", path: file.path };
1214
- }
1215
- function fileEvidenceKey(file) {
1216
- return file.sourceKind === "local" ? `local:${file.root}:${file.path}` : `repo:${file.path}`;
1217
- }
1218
- function recoveryHowForFile(file, prefix) {
1219
- if (file.sourceKind === "local") {
1220
- return `${prefix} with source.kind=local root=${file.root} path=${file.path}.`;
1221
- }
1222
- return `${prefix} with path=${file.path}.`;
1223
- }
1224
- function parseTopK(value, defaultValue) {
1225
- if (value === undefined) {
1226
- return defaultValue;
1227
- }
1228
- if (!Number.isInteger(value) || value < 1 || value > MAX_TOP_K) {
1229
- return `topK must be an integer from 1 to ${MAX_TOP_K}.`;
1230
- }
1231
- return value;
1232
- }
1233
- function matchMetadataForCandidate(candidate) {
1234
- return {
1235
- type: candidate.exactNormalizedPhrase ? "exact_phrase" : "lexical",
1236
- confidence: candidate.exactNormalizedPhrase ? 0.95 : scoreConfidence(candidate.score),
1237
- };
1238
- }
1239
- function matchMetadataForText(text, query, score) {
1240
- const normalizedText = normalizeComparableText(text);
1241
- const normalizedQuery = normalizeComparableText(query);
1242
- const exact = normalizedQuery.length > 0 && normalizedText.includes(normalizedQuery);
1243
- return {
1244
- type: exact ? "exact_phrase" : "lexical",
1245
- confidence: exact ? 0.95 : scoreConfidence(score),
1246
- };
1247
- }
1248
- function scoreConfidence(score) {
1249
- if (score === undefined || !Number.isFinite(score) || score <= 0) {
1250
- return 0.6;
1251
- }
1252
- return Math.max(0.6, Math.min(0.9, score / (score + 10)));
1253
- }
1254
- function normalizeComparableText(value) {
1255
- return value
1256
- .toLowerCase()
1257
- .replace(/[^a-z0-9_.$/-]+/g, " ")
1258
- .replace(/\s+/g, " ")
1259
- .trim();
1260
- }
1261
- function evidenceFromCandidate(candidate, query) {
1262
- const defaultStart = Math.max(0, candidate.lineIndex - DEFAULT_CONTEXT_LINES);
1263
- const defaultEnd = Math.min(candidate.file.lines.length - 1, candidate.lineIndex + DEFAULT_CONTEXT_LINES);
1264
- const lines = candidate.range ?? { start: defaultStart + 1, end: defaultEnd + 1 };
1265
- return evidenceFromRange({
1266
- id: evidenceIdFor(fileEvidenceKey(candidate.file), `${lines.start}-${lines.end}`, query),
1267
- file: candidate.file,
1268
- lines,
1269
- why: `${candidate.reason} near ${candidate.file.path}:${candidate.lineIndex + 1}.`,
1270
- window: "small",
1271
- expandable: true,
1272
- ...(candidate.exactNormalizedPhrase !== undefined
1273
- ? { exactNormalizedPhrase: candidate.exactNormalizedPhrase }
1274
- : {}),
1275
- });
1276
- }
1277
- function evidenceFromRange(options) {
1278
- const bounded = buildBoundedExcerpt({
1279
- lines: options.file.lines,
1280
- range: options.lines,
1281
- window: options.window,
1282
- caps: BOUNDED_EVIDENCE_CAPS,
1283
- ...(options.exactNormalizedPhrase !== undefined ? { exactNormalizedPhrase: options.exactNormalizedPhrase } : {}),
1284
- });
1285
- const lines = bounded.linesLabel;
1286
- const why = bounded.truncatedByLineCap
1287
- ? `${options.why} Bounded to ${lines} by the ${options.window} line cap; use retrieve lineRange or a wider expansion for more exact context.`
1288
- : options.why;
1289
- return {
1290
- id: options.id,
1291
- source: fileSourceRef(options.file),
1292
- path: options.file.path,
1293
- lines,
1294
- excerpt: bounded.excerpt,
1295
- why,
1296
- window: options.window,
1297
- expandable: options.expandable,
1298
- };
1299
- }
1300
- function parseLineRange(lines) {
1301
- const match = /^(\d+)(?:-(\d+))?$/.exec(lines);
1302
- if (!match) {
1303
- return null;
1304
- }
1305
- const start = Number(match[1]);
1306
- const end = Number(match[2] ?? match[1]);
1307
- if (!Number.isInteger(start) || !Number.isInteger(end) || start < 1 || end < start) {
1308
- return null;
1309
- }
1310
- return { start, end };
1311
- }
1312
- function expandLineRange(range, fileLineCount, expansion) {
1313
- if (expansion === "full") {
1314
- return { start: 1, end: fileLineCount };
1315
- }
1316
- const context = expansion === "lines_80" ? 80 : 30;
1317
- return {
1318
- start: Math.max(1, range.start - context),
1319
- end: Math.min(fileLineCount, range.end + context),
1320
- };
1321
- }
1322
- function windowForExpansion(expansion) {
1323
- return expansion;
1324
- }
1325
- function excerptForLineRange(lines, range, window, exactNormalizedPhrase) {
1326
- return buildBoundedExcerpt({
1327
- lines,
1328
- range,
1329
- window,
1330
- caps: BOUNDED_EVIDENCE_CAPS,
1331
- ...(exactNormalizedPhrase !== undefined ? { exactNormalizedPhrase } : {}),
1332
- }).excerpt;
1333
- }
1334
- function byteLength(text) {
1335
- return Buffer.byteLength(text, "utf8");
1336
- }
1337
- function splitLines(text) {
1338
- return text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n");
1339
- }
1340
- function decisionId(...parts) {
1341
- return `ffdec_${hash(parts.join("\0")).slice(0, 16)}`;
1342
- }
1343
- function evidenceIdFor(path, lines, query) {
1344
- return `ev_${hash(`${path}\0${lines}\0${query}`).slice(0, 16)}`;
1345
- }
1346
- function hash(value) {
1347
- return createHash("sha256").update(value).digest("hex");
1348
- }
1349
- function errorResult(preserve, reason) {
1350
- return {
1351
- toolStatus: "error",
1352
- decisionId: decisionId("repo-error", reason),
1353
- preserve,
1354
- routing: {
1355
- status: "failed",
1356
- route: "retrieve",
1357
- reason,
1358
- },
1359
- evidence: [],
1360
- recovery: {
1361
- how: "Use native read for a known whole file, or retry freeflow_search with a supported repo query.",
1362
- },
1363
- };
1364
- }