all-hands-cli 0.1.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 (305) hide show
  1. package/.allhands/README.md +75 -0
  2. package/.allhands/agents/compounder.yaml +15 -0
  3. package/.allhands/agents/coordinator.yaml +17 -0
  4. package/.allhands/agents/documentor.yaml +15 -0
  5. package/.allhands/agents/e2e-test-planner.yaml +17 -0
  6. package/.allhands/agents/emergent.yaml +22 -0
  7. package/.allhands/agents/executor.yaml +14 -0
  8. package/.allhands/agents/ideation.yaml +11 -0
  9. package/.allhands/agents/initiative-steering.yaml +19 -0
  10. package/.allhands/agents/judge.yaml +13 -0
  11. package/.allhands/agents/planner.yaml +19 -0
  12. package/.allhands/agents/pr-reviewer.yaml +15 -0
  13. package/.allhands/docs.json +5 -0
  14. package/.allhands/docs.local.json +26 -0
  15. package/.allhands/flows/COMPOUNDING.md +203 -0
  16. package/.allhands/flows/COORDINATION.md +89 -0
  17. package/.allhands/flows/CORE.md +87 -0
  18. package/.allhands/flows/DOCUMENTATION.md +218 -0
  19. package/.allhands/flows/E2E_TEST_PLAN_BUILDING.md +140 -0
  20. package/.allhands/flows/EMERGENT_PLANNING.md +57 -0
  21. package/.allhands/flows/IDEATION_SCOPING.md +154 -0
  22. package/.allhands/flows/INITIATIVE_STEERING.md +110 -0
  23. package/.allhands/flows/JUDGE_REVIEWING.md +79 -0
  24. package/.allhands/flows/PROMPT_TASK_EXECUTION.md +68 -0
  25. package/.allhands/flows/PR_REVIEWING.md +43 -0
  26. package/.allhands/flows/SPEC_PLANNING.md +216 -0
  27. package/.allhands/flows/harness/WRITING_HARNESS_FLOWS.md +27 -0
  28. package/.allhands/flows/harness/WRITING_HARNESS_KNOWLEDGE.md +27 -0
  29. package/.allhands/flows/harness/WRITING_HARNESS_ORCHESTRATION.md +27 -0
  30. package/.allhands/flows/harness/WRITING_HARNESS_SKILLS.md +27 -0
  31. package/.allhands/flows/harness/WRITING_HARNESS_TOOLS.md +27 -0
  32. package/.allhands/flows/harness/WRITING_HARNESS_VALIDATION_TOOLING.md +27 -0
  33. package/.allhands/flows/shared/CODEBASE_UNDERSTANDING.md +72 -0
  34. package/.allhands/flows/shared/CREATE_HARNESS_SPEC.md +48 -0
  35. package/.allhands/flows/shared/CREATE_SPEC.md +41 -0
  36. package/.allhands/flows/shared/CREATE_VALIDATION_TOOLING_SPEC.md +70 -0
  37. package/.allhands/flows/shared/DOCUMENTATION_DISCOVERY.md +123 -0
  38. package/.allhands/flows/shared/DOCUMENTATION_WRITER.md +101 -0
  39. package/.allhands/flows/shared/EMERGENT_REFINEMENT_ANALYSIS.md +76 -0
  40. package/.allhands/flows/shared/EXTERNAL_TECH_GUIDANCE.md +97 -0
  41. package/.allhands/flows/shared/IDEATION_CODEBASE_GROUNDING.md +49 -0
  42. package/.allhands/flows/shared/PLAN_DEEPENING.md +152 -0
  43. package/.allhands/flows/shared/PROMPT_TASKS_CURATION.md +113 -0
  44. package/.allhands/flows/shared/PROMPT_VALIDATION_REVIEW.MD +99 -0
  45. package/.allhands/flows/shared/QUICK_PREMORTEM.md +70 -0
  46. package/.allhands/flows/shared/RESEARCH_GUIDANCE.md +38 -0
  47. package/.allhands/flows/shared/REVIEW_OPTIONS_BREAKDOWN.md +68 -0
  48. package/.allhands/flows/shared/SKILL_EXTRACTION.md +84 -0
  49. package/.allhands/flows/shared/SPEC_FLOW_ANALYSIS.md +119 -0
  50. package/.allhands/flows/shared/TDD_WORKFLOW.md +109 -0
  51. package/.allhands/flows/shared/UTILIZE_VALIDATION_TOOLING.md +84 -0
  52. package/.allhands/flows/shared/WRITING_HARNESS_FLOWS.md +11 -0
  53. package/.allhands/flows/shared/WRITING_HARNESS_MCP_TOOLS.md +84 -0
  54. package/.allhands/flows/shared/jury/ARCHITECTURE_REVIEW.md +91 -0
  55. package/.allhands/flows/shared/jury/BEST_PRACTICES_REVIEW.md +80 -0
  56. package/.allhands/flows/shared/jury/CLAIM_VERIFICATION_REVIEW.md +101 -0
  57. package/.allhands/flows/shared/jury/EXPECTATIONS_FIT_REVIEW.md +78 -0
  58. package/.allhands/flows/shared/jury/MAINTAINABILITY_REVIEW.md +110 -0
  59. package/.allhands/flows/shared/jury/PROMPTS_EXPECTATIONS_FIT.md +74 -0
  60. package/.allhands/flows/shared/jury/PROMPTS_FLOW_ANALYSIS.md +92 -0
  61. package/.allhands/flows/shared/jury/PROMPTS_YAGNI.md +78 -0
  62. package/.allhands/flows/shared/jury/PROMPT_PREMORTEM.md +125 -0
  63. package/.allhands/flows/shared/jury/SECURITY_REVIEW.md +86 -0
  64. package/.allhands/flows/shared/jury/YAGNI_REVIEW.md +82 -0
  65. package/.allhands/flows/wip/DEBUG_INVESTIGATION.md +162 -0
  66. package/.allhands/flows/wip/MEMORY_RECALL.md +62 -0
  67. package/.allhands/harness/ah +131 -0
  68. package/.allhands/harness/package-lock.json +5292 -0
  69. package/.allhands/harness/package.json +52 -0
  70. package/.allhands/harness/src/__tests__/e2e/commands.test.ts +307 -0
  71. package/.allhands/harness/src/__tests__/e2e/event-loop.test.ts +539 -0
  72. package/.allhands/harness/src/__tests__/e2e/hooks.test.ts +427 -0
  73. package/.allhands/harness/src/__tests__/e2e/new-initiative-routing.test.ts +137 -0
  74. package/.allhands/harness/src/__tests__/e2e/run-e2e.ts +109 -0
  75. package/.allhands/harness/src/__tests__/e2e/specs-type.test.ts +210 -0
  76. package/.allhands/harness/src/__tests__/e2e/validation-hooks.test.ts +669 -0
  77. package/.allhands/harness/src/__tests__/e2e/validation-path-consistency.test.ts +354 -0
  78. package/.allhands/harness/src/__tests__/e2e/validation.test.ts +528 -0
  79. package/.allhands/harness/src/__tests__/harness/assertions.ts +318 -0
  80. package/.allhands/harness/src/__tests__/harness/cli-runner.ts +359 -0
  81. package/.allhands/harness/src/__tests__/harness/fixture.ts +384 -0
  82. package/.allhands/harness/src/__tests__/harness/hook-runner.ts +411 -0
  83. package/.allhands/harness/src/__tests__/harness/index.ts +122 -0
  84. package/.allhands/harness/src/cli.ts +36 -0
  85. package/.allhands/harness/src/commands/complexity.ts +177 -0
  86. package/.allhands/harness/src/commands/context7.ts +202 -0
  87. package/.allhands/harness/src/commands/docs.ts +557 -0
  88. package/.allhands/harness/src/commands/hooks.ts +24 -0
  89. package/.allhands/harness/src/commands/index.ts +51 -0
  90. package/.allhands/harness/src/commands/knowledge.ts +382 -0
  91. package/.allhands/harness/src/commands/memories.ts +302 -0
  92. package/.allhands/harness/src/commands/notify.ts +61 -0
  93. package/.allhands/harness/src/commands/oracle.ts +158 -0
  94. package/.allhands/harness/src/commands/perplexity.ts +220 -0
  95. package/.allhands/harness/src/commands/planning.ts +245 -0
  96. package/.allhands/harness/src/commands/schema.ts +73 -0
  97. package/.allhands/harness/src/commands/skills.ts +128 -0
  98. package/.allhands/harness/src/commands/solutions.ts +353 -0
  99. package/.allhands/harness/src/commands/spawn.ts +158 -0
  100. package/.allhands/harness/src/commands/specs.ts +532 -0
  101. package/.allhands/harness/src/commands/tavily.ts +226 -0
  102. package/.allhands/harness/src/commands/tools.ts +579 -0
  103. package/.allhands/harness/src/commands/trace.ts +327 -0
  104. package/.allhands/harness/src/commands/tui.ts +960 -0
  105. package/.allhands/harness/src/commands/validate.ts +143 -0
  106. package/.allhands/harness/src/commands/validation-tools.ts +108 -0
  107. package/.allhands/harness/src/hooks/context.ts +1442 -0
  108. package/.allhands/harness/src/hooks/enforcement.ts +170 -0
  109. package/.allhands/harness/src/hooks/index.ts +54 -0
  110. package/.allhands/harness/src/hooks/lifecycle.ts +229 -0
  111. package/.allhands/harness/src/hooks/notification.ts +104 -0
  112. package/.allhands/harness/src/hooks/observability.ts +551 -0
  113. package/.allhands/harness/src/hooks/session.ts +88 -0
  114. package/.allhands/harness/src/hooks/shared.ts +815 -0
  115. package/.allhands/harness/src/hooks/transcript-parser.ts +208 -0
  116. package/.allhands/harness/src/hooks/validation.ts +617 -0
  117. package/.allhands/harness/src/lib/__tests__/ctags.test.ts +244 -0
  118. package/.allhands/harness/src/lib/__tests__/docs-validation.test.ts +344 -0
  119. package/.allhands/harness/src/lib/__tests__/mcp-runtime.test.ts +190 -0
  120. package/.allhands/harness/src/lib/__tests__/schema.test.ts +861 -0
  121. package/.allhands/harness/src/lib/base-command.ts +198 -0
  122. package/.allhands/harness/src/lib/cli-daemon.ts +343 -0
  123. package/.allhands/harness/src/lib/compaction.ts +313 -0
  124. package/.allhands/harness/src/lib/ctags.ts +497 -0
  125. package/.allhands/harness/src/lib/docs-validation.ts +907 -0
  126. package/.allhands/harness/src/lib/event-loop.ts +662 -0
  127. package/.allhands/harness/src/lib/flows.ts +155 -0
  128. package/.allhands/harness/src/lib/git.ts +276 -0
  129. package/.allhands/harness/src/lib/knowledge-worker.ts +72 -0
  130. package/.allhands/harness/src/lib/knowledge.ts +810 -0
  131. package/.allhands/harness/src/lib/llm.ts +255 -0
  132. package/.allhands/harness/src/lib/mcp-client.ts +432 -0
  133. package/.allhands/harness/src/lib/mcp-daemon.ts +486 -0
  134. package/.allhands/harness/src/lib/mcp-runtime.ts +418 -0
  135. package/.allhands/harness/src/lib/notification.ts +115 -0
  136. package/.allhands/harness/src/lib/opencode/index.ts +70 -0
  137. package/.allhands/harness/src/lib/opencode/profiles.ts +300 -0
  138. package/.allhands/harness/src/lib/opencode/prompts/codesearch.md +98 -0
  139. package/.allhands/harness/src/lib/opencode/prompts/knowledge-aggregator.md +67 -0
  140. package/.allhands/harness/src/lib/opencode/runner.ts +281 -0
  141. package/.allhands/harness/src/lib/oracle.ts +926 -0
  142. package/.allhands/harness/src/lib/planning-utils.ts +150 -0
  143. package/.allhands/harness/src/lib/planning.ts +605 -0
  144. package/.allhands/harness/src/lib/pr-review.ts +225 -0
  145. package/.allhands/harness/src/lib/prompts.ts +522 -0
  146. package/.allhands/harness/src/lib/schema.ts +418 -0
  147. package/.allhands/harness/src/lib/schemas/agent-profile.ts +141 -0
  148. package/.allhands/harness/src/lib/schemas/template-vars.ts +138 -0
  149. package/.allhands/harness/src/lib/session.ts +164 -0
  150. package/.allhands/harness/src/lib/specs.ts +348 -0
  151. package/.allhands/harness/src/lib/tldr.ts +829 -0
  152. package/.allhands/harness/src/lib/tmux.ts +1051 -0
  153. package/.allhands/harness/src/lib/trace-store.ts +714 -0
  154. package/.allhands/harness/src/mcp/__tests__/index.test.ts +46 -0
  155. package/.allhands/harness/src/mcp/_template.ts +47 -0
  156. package/.allhands/harness/src/mcp/filesystem.ts +33 -0
  157. package/.allhands/harness/src/mcp/index.ts +69 -0
  158. package/.allhands/harness/src/mcp/playwright.ts +34 -0
  159. package/.allhands/harness/src/mcp/xcodebuild.ts +29 -0
  160. package/.allhands/harness/src/schemas/docs.schema.json +44 -0
  161. package/.allhands/harness/src/schemas/settings.schema.json +214 -0
  162. package/.allhands/harness/src/tui/actions.ts +227 -0
  163. package/.allhands/harness/src/tui/file-viewer-modal.ts +270 -0
  164. package/.allhands/harness/src/tui/index.ts +1574 -0
  165. package/.allhands/harness/src/tui/modal.ts +232 -0
  166. package/.allhands/harness/src/tui/prompts-pane.ts +186 -0
  167. package/.allhands/harness/src/tui/status-pane.ts +434 -0
  168. package/.allhands/harness/tsconfig.json +22 -0
  169. package/.allhands/harness/vitest.config.ts +13 -0
  170. package/.allhands/pillars.md +33 -0
  171. package/.allhands/principles.md +88 -0
  172. package/.allhands/schemas/alignment.yaml +51 -0
  173. package/.allhands/schemas/documentation.yaml +10 -0
  174. package/.allhands/schemas/prompt.yaml +92 -0
  175. package/.allhands/schemas/skill.yaml +34 -0
  176. package/.allhands/schemas/solution.yaml +131 -0
  177. package/.allhands/schemas/spec.yaml +67 -0
  178. package/.allhands/schemas/validation-suite.yaml +49 -0
  179. package/.allhands/schemas/workflow.yaml +51 -0
  180. package/.allhands/settings.json +57 -0
  181. package/.allhands/skills/claude-code-patterns/SKILL.md +60 -0
  182. package/.allhands/skills/claude-code-patterns/docs/context-hygiene.md +19 -0
  183. package/.allhands/skills/harness-maintenance/SKILL.md +449 -0
  184. package/.allhands/skills/harness-maintenance/references/core-architecture.md +187 -0
  185. package/.allhands/skills/harness-maintenance/references/harness-skills.md +87 -0
  186. package/.allhands/skills/harness-maintenance/references/knowledge-compounding.md +78 -0
  187. package/.allhands/skills/harness-maintenance/references/tools-commands-mcp-hooks.md +115 -0
  188. package/.allhands/skills/harness-maintenance/references/validation-tooling.md +77 -0
  189. package/.allhands/skills/harness-maintenance/references/writing-flows.md +84 -0
  190. package/.allhands/validation/browser-automation.md +109 -0
  191. package/.allhands/validation/xcode-automation.md +195 -0
  192. package/.allhands/workflows/documentation.md +86 -0
  193. package/.allhands/workflows/investigation.md +81 -0
  194. package/.allhands/workflows/milestone.md +91 -0
  195. package/.allhands/workflows/optimization.md +85 -0
  196. package/.allhands/workflows/refactor.md +99 -0
  197. package/.allhands/workflows/triage.md +81 -0
  198. package/.claude/README.md +1 -0
  199. package/.claude/agents/explorer.md +10 -0
  200. package/.claude/agents/researcher.md +11 -0
  201. package/.claude/agents/task-runner.md +8 -0
  202. package/.claude/settings.json +231 -0
  203. package/.env.ai.example +7 -0
  204. package/.github/workflows/npm-publish.yml +69 -0
  205. package/.internal.json +45 -0
  206. package/.tldr/config.json +11 -0
  207. package/.tldrignore +90 -0
  208. package/CLAUDE.md +6 -0
  209. package/README.md +98 -0
  210. package/bin/sync-cli.js +7552 -0
  211. package/concerns.md +7 -0
  212. package/docs/README.md +41 -0
  213. package/docs/agents/README.md +24 -0
  214. package/docs/agents/agent-configuration-system.md +86 -0
  215. package/docs/agents/execution-agents.md +50 -0
  216. package/docs/agents/knowledge-agents.md +61 -0
  217. package/docs/agents/orchestration-agent.md +57 -0
  218. package/docs/agents/planning-agents.md +84 -0
  219. package/docs/agents/quality-review-agents.md +67 -0
  220. package/docs/agents/workflow-agent-orchestration.md +69 -0
  221. package/docs/flows/README.md +44 -0
  222. package/docs/flows/compounding.md +126 -0
  223. package/docs/flows/coordination.md +72 -0
  224. package/docs/flows/core-harness-integration.md +63 -0
  225. package/docs/flows/documentation-orchestration.md +98 -0
  226. package/docs/flows/e2e-test-plan-building.md +83 -0
  227. package/docs/flows/emergent-refinement.md +104 -0
  228. package/docs/flows/flow-authoring-and-mcp-tools.md +89 -0
  229. package/docs/flows/judge-reviewing.md +112 -0
  230. package/docs/flows/plan-deepening-and-research.md +107 -0
  231. package/docs/flows/plan-review-jury.md +114 -0
  232. package/docs/flows/pr-reviewing.md +54 -0
  233. package/docs/flows/prompt-task-execution.md +119 -0
  234. package/docs/flows/spec-planning.md +162 -0
  235. package/docs/flows/type-specific-scoping-flows.md +49 -0
  236. package/docs/flows/validation-and-skills-integration.md +145 -0
  237. package/docs/flows/wip/wip-flows.md +102 -0
  238. package/docs/harness/README.md +23 -0
  239. package/docs/harness/agent-profiles.md +84 -0
  240. package/docs/harness/cli/README.md +24 -0
  241. package/docs/harness/cli/cli-entry-and-command-discovery.md +91 -0
  242. package/docs/harness/cli/docs-command.md +87 -0
  243. package/docs/harness/cli/knowledge-command.md +91 -0
  244. package/docs/harness/cli/minor-cli-commands.md +65 -0
  245. package/docs/harness/cli/oracle-command.md +113 -0
  246. package/docs/harness/cli/planning-command.md +95 -0
  247. package/docs/harness/cli/schema-and-validation-commands.md +154 -0
  248. package/docs/harness/cli/search-commands.md +97 -0
  249. package/docs/harness/cli/spawn-command.md +136 -0
  250. package/docs/harness/cli/specs-command.md +102 -0
  251. package/docs/harness/cli/tools-command.md +122 -0
  252. package/docs/harness/cli/trace-command.md +122 -0
  253. package/docs/harness/cli-daemon.md +92 -0
  254. package/docs/harness/event-loop.md +184 -0
  255. package/docs/harness/hooks/README.md +15 -0
  256. package/docs/harness/hooks/context-hooks.md +96 -0
  257. package/docs/harness/hooks/lifecycle-and-observability-hooks.md +135 -0
  258. package/docs/harness/hooks/validation-hooks.md +97 -0
  259. package/docs/harness/test-harness.md +149 -0
  260. package/docs/harness/tui.md +176 -0
  261. package/docs/memories.md +20 -0
  262. package/docs/solutions/agentic-issues/premature-agent-deletion-tui-action-dependency-20260130.md +49 -0
  263. package/docs/solutions/agentic-issues/ref-anchor-scope-mismatch-skill-references-20260131.md +55 -0
  264. package/docs/solutions/agentic-issues/tautological-tests-routing-20260131.md +52 -0
  265. package/docs/solutions/integration_issue/blocktool-output-format-mismatch-hook-runner-20260130.md +52 -0
  266. package/docs/solutions/integration_issue/dual-validation-path-divergence-schema-20260130.md +66 -0
  267. package/docs/solutions/security-issues/unsanitized-domain-path-join-20260131.md +52 -0
  268. package/docs/solutions/test-failures/event-loop-mock-ordering-checkAgentWindows-20260130.md +63 -0
  269. package/docs/sync-cli/README.md +19 -0
  270. package/docs/sync-cli/cli-entrypoint-and-commands.md +39 -0
  271. package/docs/sync-cli/commands/README.md +11 -0
  272. package/docs/sync-cli/commands/pull-manifest-command.md +36 -0
  273. package/docs/sync-cli/commands/push-command.md +84 -0
  274. package/docs/sync-cli/commands/sync-command.md +71 -0
  275. package/docs/sync-cli/systems/README.md +14 -0
  276. package/docs/sync-cli/systems/git-and-github-integration.md +49 -0
  277. package/docs/sync-cli/systems/interactive-ui.md +43 -0
  278. package/docs/sync-cli/systems/manifest-and-distribution.md +51 -0
  279. package/docs/sync-cli/systems/path-resolution.md +42 -0
  280. package/package.json +46 -0
  281. package/scripts/install-shim.sh +40 -0
  282. package/scripts/pre-pack.sh +25 -0
  283. package/specs/harness-maintenance-skill.spec.md +138 -0
  284. package/specs/roadmap/git-spec-lifecycle-management.spec.md +113 -0
  285. package/specs/sync-init-flag.spec.md +117 -0
  286. package/specs/unified-workflow-orchestration.spec.md +250 -0
  287. package/specs/validation-tooling-practice.spec.md +98 -0
  288. package/specs/workflow-domain-configuration.spec.md +265 -0
  289. package/src/commands/pull-manifest.ts +31 -0
  290. package/src/commands/push.ts +344 -0
  291. package/src/commands/sync.ts +289 -0
  292. package/src/lib/constants.ts +10 -0
  293. package/src/lib/dotfiles.ts +36 -0
  294. package/src/lib/fs-utils.ts +18 -0
  295. package/src/lib/gh.ts +40 -0
  296. package/src/lib/git.ts +63 -0
  297. package/src/lib/gitignore.ts +167 -0
  298. package/src/lib/manifest.ts +121 -0
  299. package/src/lib/marker-sync.ts +39 -0
  300. package/src/lib/paths.ts +38 -0
  301. package/src/lib/target-lines.ts +66 -0
  302. package/src/lib/ui.ts +78 -0
  303. package/src/sync-cli.ts +120 -0
  304. package/target-lines.json +23 -0
  305. package/tsconfig.json +20 -0
@@ -0,0 +1,810 @@
1
+ /**
2
+ * KnowledgeService - USearch-based semantic search for documentation.
3
+ * Uses @visheratin/web-ai-node for embeddings and usearch for HNSW indexing.
4
+ */
5
+
6
+ import { spawn } from "child_process";
7
+ import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "fs";
8
+ import matter from "gray-matter";
9
+ import { basename, extname, join, relative } from "path";
10
+ import { Index, MetricKind, ScalarKind } from "usearch";
11
+ import { loadProjectSettings } from "../hooks/shared.js";
12
+
13
+ // Types
14
+ interface DocumentMeta {
15
+ description: string;
16
+ relevant_files: string[];
17
+ token_count: number;
18
+ }
19
+
20
+ interface IndexMetadata {
21
+ id_to_path: Record<string, string>;
22
+ path_to_id: Record<string, string>;
23
+ documents: Record<string, DocumentMeta>;
24
+ next_id: number;
25
+ lastUpdated: string;
26
+ }
27
+
28
+ interface SearchResult {
29
+ resource_path: string;
30
+ similarity: number;
31
+ token_count: number;
32
+ description: string;
33
+ relevant_files: string[];
34
+ full_resource_context?: string;
35
+ }
36
+
37
+ interface ReindexResult {
38
+ files_indexed: number;
39
+ total_tokens: number;
40
+ }
41
+
42
+ interface FileChange {
43
+ path: string;
44
+ added?: boolean;
45
+ deleted?: boolean;
46
+ modified?: boolean;
47
+ }
48
+
49
+ interface IndexConfig {
50
+ name: string;
51
+ paths: string[];
52
+ extensions: string[];
53
+ description: string;
54
+ /** Whether this index expects front-matter with description/relevant_files */
55
+ hasFrontmatter: boolean;
56
+ /** Whether to strip frontmatter from content before embedding */
57
+ stripFrontmatter?: boolean;
58
+ }
59
+
60
+ // Index configurations
61
+ const INDEX_CONFIGS: Record<string, IndexConfig> = {
62
+ docs: {
63
+ name: "docs",
64
+ paths: ["docs/"],
65
+ extensions: [".md"],
66
+ description: "Project documentation and specifications",
67
+ hasFrontmatter: true,
68
+ stripFrontmatter: true,
69
+ },
70
+ roadmap: {
71
+ name: "roadmap",
72
+ paths: ["specs/roadmap/"],
73
+ extensions: [".md"],
74
+ description: "Roadmap specifications (planned work)",
75
+ hasFrontmatter: true,
76
+ stripFrontmatter: true,
77
+ },
78
+ };
79
+
80
+ export type IndexName = keyof typeof INDEX_CONFIGS;
81
+
82
+ // Config with settings override > default
83
+ const settings = loadProjectSettings();
84
+ const SEARCH_SIMILARITY_THRESHOLD = settings?.knowledge?.similarityThreshold ?? 0.65;
85
+ const SEARCH_CONTEXT_TOKEN_LIMIT = settings?.knowledge?.contextTokenLimit ?? 5000;
86
+ const SEARCH_FULL_CONTEXT_SIMILARITY_THRESHOLD = settings?.knowledge?.fullContextSimilarityThreshold ?? 0.82;
87
+
88
+ export class KnowledgeService {
89
+ private model: unknown = null;
90
+ private readonly knowledgeDir: string;
91
+ private readonly projectRoot: string;
92
+ private readonly quiet: boolean;
93
+
94
+ constructor(projectRoot: string, options?: { quiet?: boolean }) {
95
+ this.projectRoot = projectRoot;
96
+ this.quiet = options?.quiet ?? false;
97
+ // Store knowledge index in .allhands/harness/.knowledge
98
+ this.knowledgeDir = join(projectRoot, ".allhands", "harness", ".knowledge");
99
+ }
100
+
101
+ private log(message: string): void {
102
+ if (!this.quiet) {
103
+ console.error(message);
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Ensure .knowledge/ directory exists
109
+ */
110
+ ensureDir(): void {
111
+ if (!existsSync(this.knowledgeDir)) {
112
+ mkdirSync(this.knowledgeDir, { recursive: true });
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Lazy-load embedding model
118
+ * Note: web-ai-node handles its own model caching
119
+ */
120
+ async getModel(): Promise<unknown> {
121
+ if (this.model) return this.model;
122
+
123
+ this.ensureDir();
124
+ this.log("[knowledge] Loading embedding model...");
125
+ const startTime = Date.now();
126
+
127
+ const { TextModel } = await import("@visheratin/web-ai-node/text");
128
+ const modelResult = await TextModel.create("gtr-t5-quant");
129
+ this.model = modelResult.model;
130
+
131
+ this.log(`[knowledge] Model loaded in ${((Date.now() - startTime) / 1000).toFixed(1)}s`);
132
+ return this.model;
133
+ }
134
+
135
+ /**
136
+ * Generate embedding for text
137
+ */
138
+ async embed(text: string): Promise<Float32Array> {
139
+ const model = await this.getModel();
140
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
141
+ const result = await (model as any).process(text);
142
+ return new Float32Array(result.result);
143
+ }
144
+
145
+
146
+ /**
147
+ * Convert cosine distance to similarity (0-1 scale)
148
+ * Cosine distance: 0 = identical, 1 = orthogonal, 2 = opposite
149
+ */
150
+ distanceToSimilarity(distance: number): number {
151
+ return 1 - distance / 2;
152
+ }
153
+
154
+ /**
155
+ * Get index file paths for a specific index
156
+ */
157
+ private getIndexPaths(indexName: IndexName): { index: string; meta: string } {
158
+ return {
159
+ index: join(this.knowledgeDir, `${indexName}.usearch`),
160
+ meta: join(this.knowledgeDir, `${indexName}.meta.json`),
161
+ };
162
+ }
163
+
164
+ /**
165
+ * Get config for a specific index
166
+ */
167
+ private getIndexConfig(indexName: IndexName): IndexConfig {
168
+ const config = INDEX_CONFIGS[indexName];
169
+ if (!config) {
170
+ throw new Error(`Unknown index: ${indexName}. Available: ${Object.keys(INDEX_CONFIGS).join(", ")}`);
171
+ }
172
+ return config;
173
+ }
174
+
175
+ /**
176
+ * Create empty index metadata
177
+ */
178
+ private createEmptyMetadata(): IndexMetadata {
179
+ return {
180
+ id_to_path: {},
181
+ path_to_id: {},
182
+ documents: {},
183
+ next_id: 0,
184
+ lastUpdated: new Date().toISOString(),
185
+ };
186
+ }
187
+
188
+ /**
189
+ * Create a new USearch index
190
+ */
191
+ private createIndex(): Index {
192
+ return new Index(
193
+ 768, // dimensions
194
+ MetricKind.Cos, // metric
195
+ ScalarKind.F32, // quantization
196
+ 16 // connectivity
197
+ );
198
+ }
199
+
200
+ /**
201
+ * Load index + metadata from disk
202
+ */
203
+ async loadIndex(indexName: IndexName): Promise<{ index: Index; meta: IndexMetadata }> {
204
+ const paths = this.getIndexPaths(indexName);
205
+
206
+ if (!existsSync(paths.index) || !existsSync(paths.meta)) {
207
+ return {
208
+ index: this.createIndex(),
209
+ meta: this.createEmptyMetadata(),
210
+ };
211
+ }
212
+
213
+ const index = this.createIndex();
214
+ index.load(paths.index);
215
+
216
+ const meta: IndexMetadata = JSON.parse(readFileSync(paths.meta, "utf-8"));
217
+ return { index, meta };
218
+ }
219
+
220
+ /**
221
+ * Save index + metadata to disk
222
+ */
223
+ async saveIndex(indexName: IndexName, index: Index, meta: IndexMetadata): Promise<void> {
224
+ this.ensureDir();
225
+ const paths = this.getIndexPaths(indexName);
226
+
227
+ meta.lastUpdated = new Date().toISOString();
228
+ index.save(paths.index);
229
+ writeFileSync(paths.meta, JSON.stringify(meta, null, 2));
230
+ }
231
+
232
+ /**
233
+ * Estimate token count (rough approximation: 1 token ≈ 4 chars)
234
+ */
235
+ private estimateTokens(text: string): number {
236
+ return Math.ceil(text.length / 4);
237
+ }
238
+
239
+ /**
240
+ * Discover files for an index based on config
241
+ */
242
+ private discoverFiles(config: IndexConfig): string[] {
243
+ const files: string[] = [];
244
+
245
+ for (const configPath of config.paths) {
246
+ const fullPath = join(this.projectRoot, configPath);
247
+
248
+ if (!existsSync(fullPath)) continue;
249
+
250
+ const stat = statSync(fullPath);
251
+ if (stat.isFile()) {
252
+ if (config.extensions.includes(extname(fullPath))) {
253
+ files.push(configPath);
254
+ }
255
+ } else if (stat.isDirectory()) {
256
+ this.walkDir(fullPath, config.extensions, files, this.projectRoot);
257
+ }
258
+ }
259
+
260
+ return files;
261
+ }
262
+
263
+ /**
264
+ * Recursively walk directory and collect files
265
+ */
266
+ private walkDir(
267
+ dir: string,
268
+ extensions: string[],
269
+ files: string[],
270
+ projectRoot: string
271
+ ): void {
272
+ const entries = readdirSync(dir, { withFileTypes: true });
273
+
274
+ for (const entry of entries) {
275
+ const fullPath = join(dir, entry.name);
276
+
277
+ if (entry.isDirectory()) {
278
+ // Skip node_modules and hidden dirs (except .allhands)
279
+ if (entry.name === "node_modules" || (entry.name.startsWith(".") && entry.name !== ".allhands")) {
280
+ continue;
281
+ }
282
+ this.walkDir(fullPath, extensions, files, projectRoot);
283
+ } else if (entry.isFile() && extensions.includes(extname(entry.name))) {
284
+ // Exclude memories.md from docs indexing - project-specific learnings, not indexed for semantic search
285
+ if (entry.name === "memories.md") continue;
286
+ files.push(relative(projectRoot, fullPath));
287
+ }
288
+ }
289
+ }
290
+
291
+ /**
292
+ * Index a single document
293
+ */
294
+ async indexDocument(
295
+ index: Index,
296
+ meta: IndexMetadata,
297
+ path: string,
298
+ content: string,
299
+ frontMatterData: Record<string, unknown>
300
+ ): Promise<bigint> {
301
+ // Assign or reuse ID, removing old entry if exists
302
+ let id: bigint;
303
+ if (meta.path_to_id[path]) {
304
+ id = BigInt(meta.path_to_id[path]);
305
+ // Remove old entry before re-adding (usearch doesn't allow duplicate keys)
306
+ index.remove(id);
307
+ } else {
308
+ id = BigInt(meta.next_id++);
309
+ meta.id_to_path[id.toString()] = path;
310
+ meta.path_to_id[path] = id.toString();
311
+ }
312
+
313
+ // Generate embedding
314
+ const embedding = await this.embed(content);
315
+
316
+ // Add to index
317
+ index.add(id, embedding);
318
+
319
+ // Store metadata
320
+ meta.documents[path] = {
321
+ description: (frontMatterData.description as string) || "",
322
+ relevant_files: (frontMatterData.relevant_files as string[]) || [],
323
+ token_count: this.estimateTokens(content),
324
+ };
325
+
326
+ return id;
327
+ }
328
+
329
+ /**
330
+ * Search an index with similarity computation
331
+ * @param indexName - Which index to search (docs, specs)
332
+ * @param query - Search query
333
+ * @param k - Max results to return
334
+ * @param metadataOnly - If true, only return file paths and descriptions (no full_resource_context)
335
+ */
336
+ async search(indexName: IndexName, query: string, k: number = 50, metadataOnly: boolean = false): Promise<SearchResult[]> {
337
+ const { index, meta } = await this.loadIndex(indexName);
338
+
339
+ if (Object.keys(meta.documents).length === 0) {
340
+ return [];
341
+ }
342
+
343
+ // Generate query embedding
344
+ const queryEmbedding = await this.embed(query);
345
+
346
+ // Over-fetch to compensate for deleted entries that remain in vector index
347
+ // (USearch doesn't support deletion, so we filter in metadata post-search)
348
+ const searchK = Math.min(k * 2, index.size());
349
+
350
+ // Search (1 thread for CLI usage)
351
+ const searchResult = index.search(queryEmbedding, searchK, 1);
352
+ const keys = searchResult.keys;
353
+ const distances = searchResult.distances;
354
+
355
+ // Convert to results
356
+ const results: SearchResult[] = [];
357
+ let totalTokens = 0;
358
+
359
+ for (let i = 0; i < keys.length; i++) {
360
+ const id = keys[i].toString();
361
+ const distance = distances[i];
362
+ const similarity = this.distanceToSimilarity(distance);
363
+
364
+ // Filter by threshold
365
+ if (similarity < SEARCH_SIMILARITY_THRESHOLD) continue;
366
+
367
+ const path = meta.id_to_path[id];
368
+ if (!path) continue;
369
+
370
+ const docMeta = meta.documents[path];
371
+ if (!docMeta) continue;
372
+
373
+ // Check token limit
374
+ if (totalTokens + docMeta.token_count > SEARCH_CONTEXT_TOKEN_LIMIT) continue;
375
+ totalTokens += docMeta.token_count;
376
+
377
+ const result: SearchResult = {
378
+ resource_path: path,
379
+ similarity,
380
+ token_count: docMeta.token_count,
381
+ description: docMeta.description,
382
+ relevant_files: docMeta.relevant_files,
383
+ };
384
+
385
+ // Include full context for high-similarity results (unless metadata-only mode)
386
+ if (!metadataOnly && similarity >= SEARCH_FULL_CONTEXT_SIMILARITY_THRESHOLD) {
387
+ const fullPath = join(this.projectRoot, path);
388
+ if (existsSync(fullPath)) {
389
+ result.full_resource_context = readFileSync(fullPath, "utf-8");
390
+ }
391
+ }
392
+
393
+ results.push(result);
394
+
395
+ // Stop once we have enough results (we over-fetched to handle deleted entries)
396
+ if (results.length >= k) break;
397
+ }
398
+
399
+ return results;
400
+ }
401
+
402
+ /**
403
+ * Full reindex of a specific index
404
+ */
405
+ async reindexAll(indexName: IndexName): Promise<ReindexResult> {
406
+ this.ensureDir();
407
+ const config = this.getIndexConfig(indexName);
408
+ const startTime = Date.now();
409
+ this.log(`[knowledge] Reindexing ${indexName}...`);
410
+
411
+ // Create fresh index
412
+ const index = this.createIndex();
413
+ const meta = this.createEmptyMetadata();
414
+
415
+ // Discover and index files
416
+ const files = this.discoverFiles(config);
417
+ this.log(`[knowledge] Found ${files.length} files`);
418
+ let totalTokens = 0;
419
+
420
+ for (let i = 0; i < files.length; i++) {
421
+ const filePath = files[i];
422
+ const fullPath = join(this.projectRoot, filePath);
423
+ const rawContent = readFileSync(fullPath, "utf-8");
424
+
425
+ // Parse front-matter
426
+ let frontMatter: Record<string, unknown> = {};
427
+ let contentForEmbedding = rawContent;
428
+
429
+ if (filePath.endsWith(".md")) {
430
+ try {
431
+ const parsed = matter(rawContent);
432
+ frontMatter = parsed.data;
433
+ // Strip frontmatter from content for embedding if configured
434
+ if (config.stripFrontmatter) {
435
+ contentForEmbedding = parsed.content;
436
+ }
437
+ } catch {
438
+ // Skip files with invalid front-matter
439
+ }
440
+ }
441
+
442
+ this.log(`[knowledge] Embedding ${i + 1}/${files.length}: ${filePath}`);
443
+ await this.indexDocument(index, meta, filePath, contentForEmbedding, frontMatter);
444
+ totalTokens += meta.documents[filePath].token_count;
445
+ }
446
+
447
+ // Save
448
+ await this.saveIndex(indexName, index, meta);
449
+ const duration = ((Date.now() - startTime) / 1000).toFixed(1);
450
+ this.log(`[knowledge] Reindex complete: ${files.length} files, ${totalTokens} tokens in ${duration}s`);
451
+
452
+ return {
453
+ files_indexed: files.length,
454
+ total_tokens: totalTokens,
455
+ };
456
+ }
457
+
458
+ /**
459
+ * Reindex all configured indexes
460
+ */
461
+ async reindexAllIndexes(): Promise<Record<string, ReindexResult>> {
462
+ const results: Record<string, ReindexResult> = {};
463
+ for (const indexName of Object.keys(INDEX_CONFIGS) as IndexName[]) {
464
+ results[indexName] = await this.reindexAll(indexName);
465
+ }
466
+ return results;
467
+ }
468
+
469
+ /**
470
+ * Incremental reindex from changed files for a specific index
471
+ */
472
+ async reindexFromChanges(indexName: IndexName, changes: FileChange[]): Promise<{
473
+ success: boolean;
474
+ message: string;
475
+ files: { path: string; action: string }[];
476
+ }> {
477
+ const config = this.getIndexConfig(indexName);
478
+ this.log(`[knowledge] Incremental reindex (${indexName}): ${changes.length} change(s)`);
479
+ const startTime = Date.now();
480
+
481
+ const { index, meta } = await this.loadIndex(indexName);
482
+ const processedFiles: { path: string; action: string }[] = [];
483
+
484
+ for (const change of changes) {
485
+ const { path, added, deleted, modified } = change;
486
+
487
+ // Check if file matches config (excluding memories.md)
488
+ const matchesConfig = config.paths.some((p: string) => path.startsWith(p)) &&
489
+ config.extensions.includes(extname(path)) &&
490
+ basename(path) !== "memories.md";
491
+
492
+ if (!matchesConfig) continue;
493
+
494
+ if (deleted) {
495
+ // Remove from index
496
+ const id = meta.path_to_id[path];
497
+ if (id) {
498
+ // Note: USearch doesn't have a remove method in basic API
499
+ // We mark as deleted in metadata
500
+ delete meta.id_to_path[id];
501
+ delete meta.path_to_id[path];
502
+ delete meta.documents[path];
503
+ processedFiles.push({ path, action: "deleted" });
504
+ this.log(`[knowledge] Deleted: ${path}`);
505
+ }
506
+ } else if (added || modified) {
507
+ const fullPath = join(this.projectRoot, path);
508
+ if (!existsSync(fullPath)) continue;
509
+
510
+ const rawContent = readFileSync(fullPath, "utf-8");
511
+ let frontMatter: Record<string, unknown> = {};
512
+ let contentForEmbedding = rawContent;
513
+
514
+ // Process front-matter
515
+ if (path.endsWith(".md")) {
516
+ try {
517
+ const parsed = matter(rawContent);
518
+ frontMatter = parsed.data;
519
+ // Strip frontmatter from content for embedding if configured
520
+ if (config.stripFrontmatter) {
521
+ contentForEmbedding = parsed.content;
522
+ }
523
+ } catch {
524
+ // Skip files with invalid front-matter
525
+ }
526
+ }
527
+
528
+ // Index document
529
+ const action = added ? "added" : "modified";
530
+ this.log(`[knowledge] Embedding (${action}): ${path}`);
531
+ await this.indexDocument(index, meta, path, contentForEmbedding, frontMatter);
532
+ processedFiles.push({ path, action });
533
+ }
534
+ }
535
+
536
+ // Save updated index
537
+ await this.saveIndex(indexName, index, meta);
538
+ const duration = ((Date.now() - startTime) / 1000).toFixed(1);
539
+ this.log(`[knowledge] Incremental reindex complete: ${processedFiles.length} file(s) in ${duration}s`);
540
+
541
+ return {
542
+ success: true,
543
+ message: "Index updated successfully",
544
+ files: processedFiles,
545
+ };
546
+ }
547
+
548
+ /**
549
+ * Check if a specific index exists
550
+ */
551
+ indexExists(indexName: IndexName): boolean {
552
+ const paths = this.getIndexPaths(indexName);
553
+ return existsSync(paths.index) && existsSync(paths.meta);
554
+ }
555
+
556
+ /**
557
+ * Check if a specific index exists (async wrapper for backward compat)
558
+ */
559
+ async checkIndex(indexName: IndexName): Promise<{ exists: boolean }> {
560
+ return { exists: this.indexExists(indexName) };
561
+ }
562
+
563
+ /**
564
+ * Get file changes since last index update for a specific index.
565
+ * Compares file modification times against last index update timestamp.
566
+ */
567
+ getChangesFromGit(indexName: IndexName): FileChange[] {
568
+ const config = this.getIndexConfig(indexName);
569
+ const paths = this.getIndexPaths(indexName);
570
+
571
+ // If no metadata file, can't do incremental - need full reindex
572
+ if (!existsSync(paths.meta)) {
573
+ return [];
574
+ }
575
+
576
+ const meta: IndexMetadata = JSON.parse(readFileSync(paths.meta, "utf-8"));
577
+ const lastUpdated = meta.lastUpdated;
578
+
579
+ if (!lastUpdated) {
580
+ return [];
581
+ }
582
+
583
+ const lastUpdateTime = new Date(lastUpdated).getTime();
584
+ const changes: FileChange[] = [];
585
+ const indexedPaths = new Set(Object.keys(meta.path_to_id));
586
+
587
+ // Check each configured path for changes
588
+ for (const configPath of config.paths) {
589
+ const fullConfigPath = join(this.projectRoot, configPath);
590
+ if (!existsSync(fullConfigPath)) continue;
591
+
592
+ // Find all matching files and check their modification times
593
+ this.findFilesRecursive(fullConfigPath, config.extensions).forEach(filePath => {
594
+ const relativePath = relative(this.projectRoot, filePath);
595
+
596
+ try {
597
+ const stats = statSync(filePath);
598
+ const fileModTime = stats.mtimeMs;
599
+
600
+ if (!indexedPaths.has(relativePath)) {
601
+ // New file not in index
602
+ changes.push({ path: relativePath, added: true });
603
+ } else if (fileModTime > lastUpdateTime) {
604
+ // File modified since last index
605
+ changes.push({ path: relativePath, modified: true });
606
+ }
607
+ } catch {
608
+ // File access error - skip
609
+ }
610
+ });
611
+ }
612
+
613
+ // Check for deleted files (in index but not on disk)
614
+ Array.from(indexedPaths).forEach(indexedPath => {
615
+ const fullPath = join(this.projectRoot, indexedPath);
616
+ if (!existsSync(fullPath)) {
617
+ changes.push({ path: indexedPath, deleted: true });
618
+ }
619
+ });
620
+
621
+ return changes;
622
+ }
623
+
624
+ /**
625
+ * Find files recursively matching extensions
626
+ */
627
+ private findFilesRecursive(dir: string, extensions: string[]): string[] {
628
+ const files: string[] = [];
629
+ if (!existsSync(dir)) return files;
630
+
631
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
632
+ const fullPath = join(dir, entry.name);
633
+ if (entry.isDirectory()) {
634
+ files.push(...this.findFilesRecursive(fullPath, extensions));
635
+ } else if (extensions.some(ext => entry.name.endsWith(ext)) && entry.name !== "memories.md") {
636
+ files.push(fullPath);
637
+ }
638
+ }
639
+ return files;
640
+ }
641
+
642
+ /**
643
+ * Check status of all indexes
644
+ */
645
+ async checkAllIndexes(): Promise<Record<string, { exists: boolean }>> {
646
+ const results: Record<string, { exists: boolean }> = {};
647
+ for (const indexName of Object.keys(INDEX_CONFIGS) as IndexName[]) {
648
+ results[indexName] = await this.checkIndex(indexName);
649
+ }
650
+ return results;
651
+ }
652
+
653
+ /**
654
+ * Get available index names
655
+ */
656
+ static getIndexNames(): string[] {
657
+ return Object.keys(INDEX_CONFIGS);
658
+ }
659
+ }
660
+
661
+ /**
662
+ * Spawn a child process to run reindexAll in an isolated worker.
663
+ * The ONNX model loads and dies entirely within the child process.
664
+ */
665
+ export async function reindexAllInWorker(
666
+ projectRoot: string,
667
+ indexName: IndexName,
668
+ onProgress?: (message: string) => void,
669
+ ): Promise<ReindexResult> {
670
+ const workerPath = join(import.meta.dirname, "knowledge-worker.ts");
671
+
672
+ return new Promise((resolve) => {
673
+ const child = spawn("npx", ["tsx", workerPath, "reindexAll", indexName, projectRoot], {
674
+ cwd: join(projectRoot, ".allhands", "harness"),
675
+ stdio: ["pipe", "pipe", "pipe"],
676
+ });
677
+
678
+ let stdoutBuffer = "";
679
+ let stderrBuffer = "";
680
+
681
+ child.stderr?.on("data", (data: Buffer) => {
682
+ stderrBuffer += data.toString();
683
+ const lines = stderrBuffer.split("\n");
684
+ stderrBuffer = lines.pop() || "";
685
+ for (const line of lines) {
686
+ if (line.trim() && onProgress) {
687
+ onProgress(line.trim());
688
+ }
689
+ }
690
+ });
691
+
692
+ child.stdout?.on("data", (data: Buffer) => {
693
+ stdoutBuffer += data.toString();
694
+ });
695
+
696
+ // 5 minute timeout (matching TLDR pattern)
697
+ const timeout = setTimeout(() => {
698
+ child.kill();
699
+ resolve({ files_indexed: 0, total_tokens: 0 });
700
+ }, 300000);
701
+
702
+ child.on("close", (code) => {
703
+ clearTimeout(timeout);
704
+ // Flush remaining stderr
705
+ if (stderrBuffer.trim() && onProgress) {
706
+ onProgress(stderrBuffer.trim());
707
+ }
708
+
709
+ if (code === 0 && stdoutBuffer.trim()) {
710
+ try {
711
+ const result = JSON.parse(stdoutBuffer.trim());
712
+ if (result.success) {
713
+ resolve({
714
+ files_indexed: result.files_indexed ?? 0,
715
+ total_tokens: result.total_tokens ?? 0,
716
+ });
717
+ return;
718
+ }
719
+ } catch {
720
+ // Parse error - fall through
721
+ }
722
+ }
723
+ resolve({ files_indexed: 0, total_tokens: 0 });
724
+ });
725
+
726
+ child.on("error", () => {
727
+ clearTimeout(timeout);
728
+ resolve({ files_indexed: 0, total_tokens: 0 });
729
+ });
730
+ });
731
+ }
732
+
733
+ /**
734
+ * Spawn a child process to run reindexFromChanges in an isolated worker.
735
+ * Changes are passed via stdin as JSON.
736
+ */
737
+ export async function reindexFromChangesInWorker(
738
+ projectRoot: string,
739
+ indexName: IndexName,
740
+ changes: FileChange[],
741
+ onProgress?: (message: string) => void,
742
+ ): Promise<{ success: boolean; message: string; files: { path: string; action: string }[] }> {
743
+ const workerPath = join(import.meta.dirname, "knowledge-worker.ts");
744
+
745
+ return new Promise((resolve) => {
746
+ const child = spawn("npx", ["tsx", workerPath, "reindexFromChanges", indexName, projectRoot], {
747
+ cwd: join(projectRoot, ".allhands", "harness"),
748
+ stdio: ["pipe", "pipe", "pipe"],
749
+ });
750
+
751
+ let stdoutBuffer = "";
752
+ let stderrBuffer = "";
753
+
754
+ child.stderr?.on("data", (data: Buffer) => {
755
+ stderrBuffer += data.toString();
756
+ const lines = stderrBuffer.split("\n");
757
+ stderrBuffer = lines.pop() || "";
758
+ for (const line of lines) {
759
+ if (line.trim() && onProgress) {
760
+ onProgress(line.trim());
761
+ }
762
+ }
763
+ });
764
+
765
+ child.stdout?.on("data", (data: Buffer) => {
766
+ stdoutBuffer += data.toString();
767
+ });
768
+
769
+ // Write changes to stdin
770
+ child.stdin?.write(JSON.stringify(changes));
771
+ child.stdin?.end();
772
+
773
+ // 5 minute timeout
774
+ const timeout = setTimeout(() => {
775
+ child.kill();
776
+ resolve({ success: false, message: "Worker timed out", files: [] });
777
+ }, 300000);
778
+
779
+ child.on("close", (code) => {
780
+ clearTimeout(timeout);
781
+ // Flush remaining stderr
782
+ if (stderrBuffer.trim() && onProgress) {
783
+ onProgress(stderrBuffer.trim());
784
+ }
785
+
786
+ if (code === 0 && stdoutBuffer.trim()) {
787
+ try {
788
+ const result = JSON.parse(stdoutBuffer.trim());
789
+ resolve({
790
+ success: result.success ?? false,
791
+ message: result.message ?? "Worker completed",
792
+ files: result.files ?? [],
793
+ });
794
+ return;
795
+ } catch {
796
+ // Parse error - fall through
797
+ }
798
+ }
799
+ resolve({ success: false, message: "Worker failed", files: [] });
800
+ });
801
+
802
+ child.on("error", () => {
803
+ clearTimeout(timeout);
804
+ resolve({ success: false, message: "Worker spawn failed", files: [] });
805
+ });
806
+ });
807
+ }
808
+
809
+ export { INDEX_CONFIGS };
810
+ export type { DocumentMeta, FileChange, IndexMetadata, ReindexResult, SearchResult };