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,382 @@
1
+ /**
2
+ * Knowledge commands - semantic search and indexing for docs and roadmap.
3
+ *
4
+ * Commands:
5
+ * ah knowledge docs search <query> [--metadata-only]
6
+ * ah knowledge roadmap search <query> [--metadata-only]
7
+ * ah knowledge docs reindex
8
+ * ah knowledge roadmap reindex
9
+ * ah knowledge reindex (all indexes)
10
+ * ah knowledge status (all indexes)
11
+ */
12
+
13
+ import { spawnSync } from "child_process";
14
+ import { Command } from "commander";
15
+ import { readFileSync } from "fs";
16
+ import { dirname, join } from "path";
17
+ import { fileURLToPath } from "url";
18
+ import {
19
+ AgentRunner,
20
+ type AggregatorOutput,
21
+ type SearchResult,
22
+ } from "../lib/opencode/index.js";
23
+ import {
24
+ INDEX_CONFIGS,
25
+ KnowledgeService,
26
+ type FileChange,
27
+ type IndexName,
28
+ } from "../lib/knowledge.js";
29
+ import { BaseCommand, CommandResult, tracedAction } from "../lib/base-command.js";
30
+ import { getBaseBranch } from "../lib/git.js";
31
+
32
+ const getProjectRoot = (): string => {
33
+ return process.env.PROJECT_ROOT || process.cwd();
34
+ };
35
+
36
+ // Load aggregator prompt from file
37
+ const __dirname = dirname(fileURLToPath(import.meta.url));
38
+ const AGGREGATOR_PROMPT_PATH = join(__dirname, "../lib/opencode/prompts/knowledge-aggregator.md");
39
+
40
+ const getAggregatorPrompt = (): string => {
41
+ return readFileSync(AGGREGATOR_PROMPT_PATH, "utf-8");
42
+ };
43
+
44
+ const DEFAULT_TOKEN_THRESHOLD = 3500;
45
+
46
+ /**
47
+ * Auto-detect file changes since branch diverged from base for a specific index.
48
+ */
49
+ function getChangesFromGit(indexName: IndexName): FileChange[] {
50
+ const config = INDEX_CONFIGS[indexName];
51
+ if (!config) return [];
52
+
53
+ const baseBranch = getBaseBranch();
54
+ const cwd = getProjectRoot();
55
+
56
+ // Get merge-base commit
57
+ const mergeBaseResult = spawnSync("git", ["merge-base", baseBranch, "HEAD"], {
58
+ encoding: "utf-8",
59
+ cwd,
60
+ stdio: ['pipe', 'pipe', 'pipe'],
61
+ });
62
+
63
+ if (mergeBaseResult.status !== 0) {
64
+ return [];
65
+ }
66
+
67
+ const mergeBase = mergeBaseResult.stdout.trim();
68
+
69
+ // Get changed files since merge-base, filtered to index paths
70
+ const diffResult = spawnSync(
71
+ "git",
72
+ ["diff", "--name-status", `${mergeBase}..HEAD`, "--", ...config.paths],
73
+ { encoding: "utf-8", cwd, stdio: ['pipe', 'pipe', 'pipe'] }
74
+ );
75
+
76
+ if (diffResult.status !== 0 || !diffResult.stdout.trim()) {
77
+ return [];
78
+ }
79
+
80
+ const changes: FileChange[] = [];
81
+ const lines = diffResult.stdout.trim().split("\n");
82
+
83
+ for (const line of lines) {
84
+ const [status, filePath] = line.split("\t");
85
+ if (!filePath) continue;
86
+
87
+ // Check extension
88
+ const ext = "." + filePath.split(".").pop();
89
+ if (!config.extensions.includes(ext)) continue;
90
+
91
+ // Skip memories.md (project-specific learnings, not indexed)
92
+ if (filePath.endsWith("memories.md")) continue;
93
+
94
+ if (status === "A") {
95
+ changes.push({ path: filePath, added: true });
96
+ } else if (status === "M") {
97
+ changes.push({ path: filePath, modified: true });
98
+ } else if (status === "D") {
99
+ changes.push({ path: filePath, deleted: true });
100
+ }
101
+ }
102
+
103
+ return changes;
104
+ }
105
+
106
+ /**
107
+ * Search command - searches a specific index
108
+ */
109
+ class SearchCommand extends BaseCommand {
110
+ readonly name = "search";
111
+ readonly description: string;
112
+ private readonly indexName: IndexName;
113
+
114
+ constructor(indexName: IndexName) {
115
+ super();
116
+ this.indexName = indexName;
117
+ this.description = `Semantic search ${indexName}`;
118
+ }
119
+
120
+ defineArguments(cmd: Command): void {
121
+ cmd
122
+ .argument("<query>", "Descriptive phrase (e.g. 'how to handle API authentication')")
123
+ .option("--metadata-only", "Return only file paths and descriptions (no full content)")
124
+ .option("--no-aggregate", "Disable aggregation entirely");
125
+ }
126
+
127
+ async execute(args: Record<string, unknown>): Promise<CommandResult> {
128
+ const query = args.query as string;
129
+ const metadataOnly = !!args.metadataOnly;
130
+ const noAggregate = !!args.noAggregate;
131
+
132
+ if (!query) {
133
+ return this.error("validation_error", "query is required");
134
+ }
135
+
136
+ const projectRoot = getProjectRoot();
137
+
138
+ try {
139
+ const service = new KnowledgeService(projectRoot);
140
+ const results = await service.search(this.indexName, query, 50, metadataOnly);
141
+
142
+ // Skip aggregation if metadata-only or explicitly disabled
143
+ if (metadataOnly || noAggregate) {
144
+ return this.success({
145
+ index: this.indexName,
146
+ query,
147
+ metadata_only: metadataOnly,
148
+ results,
149
+ result_count: results.length,
150
+ });
151
+ }
152
+
153
+ // Check if aggregation is needed (token threshold)
154
+ const totalTokens = results.reduce((sum, r) => sum + r.token_count, 0);
155
+ if (totalTokens <= DEFAULT_TOKEN_THRESHOLD) {
156
+ return this.success({
157
+ index: this.indexName,
158
+ query,
159
+ results,
160
+ result_count: results.length,
161
+ aggregated: false,
162
+ });
163
+ }
164
+
165
+ // Aggregate with AI
166
+ const runner = new AgentRunner(projectRoot);
167
+ const { fullResults, minimizedResults } = results.reduce(
168
+ (acc, r) => {
169
+ if (r.full_resource_context) {
170
+ acc.fullResults.push(r);
171
+ } else {
172
+ acc.minimizedResults.push(r);
173
+ }
174
+ return acc;
175
+ },
176
+ { fullResults: [] as SearchResult[], minimizedResults: [] as SearchResult[] }
177
+ );
178
+
179
+ const userMessage = JSON.stringify({
180
+ query,
181
+ full_results: fullResults,
182
+ minimized_results: minimizedResults.map(r => ({
183
+ resource_path: r.resource_path,
184
+ similarity: r.similarity,
185
+ description: r.description,
186
+ relevant_files: r.relevant_files,
187
+ })),
188
+ });
189
+
190
+ const agentResult = await runner.run<AggregatorOutput>(
191
+ {
192
+ name: "knowledge-aggregator",
193
+ systemPrompt: getAggregatorPrompt(),
194
+ timeoutMs: 60000,
195
+ steps: 5,
196
+ },
197
+ userMessage
198
+ );
199
+
200
+ if (!agentResult.success) {
201
+ // Fall back to raw results on aggregation failure
202
+ return this.success({
203
+ index: this.indexName,
204
+ query,
205
+ results,
206
+ result_count: results.length,
207
+ aggregated: false,
208
+ aggregation_error: agentResult.error,
209
+ });
210
+ }
211
+
212
+ return this.success({
213
+ index: this.indexName,
214
+ query,
215
+ aggregated: true,
216
+ insight: agentResult.data!.insight,
217
+ lsp_entry_points: agentResult.data!.lsp_entry_points,
218
+ design_notes: agentResult.data!.design_notes,
219
+ source_results: results.length,
220
+ });
221
+ } catch (error) {
222
+ const message = error instanceof Error ? error.message : String(error);
223
+ return this.error("search_error", message);
224
+ }
225
+ }
226
+ }
227
+
228
+ /**
229
+ * Reindex command - rebuilds an index
230
+ */
231
+ class ReindexCommand extends BaseCommand {
232
+ readonly name = "reindex";
233
+ readonly description: string;
234
+ private readonly indexName: IndexName | "all";
235
+
236
+ constructor(indexName: IndexName | "all") {
237
+ super();
238
+ this.indexName = indexName;
239
+ this.description = indexName === "all"
240
+ ? "Rebuild all indexes"
241
+ : `Rebuild ${indexName} index`;
242
+ }
243
+
244
+ defineArguments(cmd: Command): void {
245
+ cmd.option("--from-changes", "Only reindex changed files since branch diverged from base");
246
+ }
247
+
248
+ async execute(args: Record<string, unknown>): Promise<CommandResult> {
249
+ const fromChanges = !!args.fromChanges;
250
+ const projectRoot = getProjectRoot();
251
+ const service = new KnowledgeService(projectRoot);
252
+
253
+ try {
254
+ if (this.indexName === "all") {
255
+ if (fromChanges) {
256
+ const results: Record<string, unknown> = {};
257
+ for (const name of KnowledgeService.getIndexNames()) {
258
+ const changes = getChangesFromGit(name as IndexName);
259
+ if (changes.length > 0) {
260
+ results[name] = await service.reindexFromChanges(name as IndexName, changes);
261
+ } else {
262
+ results[name] = { skipped: true, reason: "no changes detected" };
263
+ }
264
+ }
265
+ return this.success({ indexes: results });
266
+ } else {
267
+ const results = await service.reindexAllIndexes();
268
+ return this.success({ indexes: results });
269
+ }
270
+ } else {
271
+ if (fromChanges) {
272
+ const changes = getChangesFromGit(this.indexName);
273
+ if (changes.length === 0) {
274
+ return this.success({
275
+ index: this.indexName,
276
+ skipped: true,
277
+ reason: "no changes detected"
278
+ });
279
+ }
280
+ const result = await service.reindexFromChanges(this.indexName, changes);
281
+ return this.success({ index: this.indexName, ...result });
282
+ } else {
283
+ const result = await service.reindexAll(this.indexName);
284
+ return this.success({ index: this.indexName, ...result });
285
+ }
286
+ }
287
+ } catch (error) {
288
+ const message = error instanceof Error ? error.message : String(error);
289
+ return this.error("reindex_error", message);
290
+ }
291
+ }
292
+ }
293
+
294
+ /**
295
+ * Status command - check index status
296
+ */
297
+ class StatusCommand extends BaseCommand {
298
+ readonly name = "status";
299
+ readonly description = "Check status of all indexes";
300
+
301
+ defineArguments(_cmd: Command): void {
302
+ // No arguments
303
+ }
304
+
305
+ async execute(_args: Record<string, unknown>): Promise<CommandResult> {
306
+ const projectRoot = getProjectRoot();
307
+ const service = new KnowledgeService(projectRoot);
308
+
309
+ try {
310
+ const results = await service.checkAllIndexes();
311
+ return this.success({ indexes: results });
312
+ } catch (error) {
313
+ const message = error instanceof Error ? error.message : String(error);
314
+ return this.error("status_error", message);
315
+ }
316
+ }
317
+ }
318
+
319
+ /**
320
+ * Register knowledge commands on the given commander program.
321
+ */
322
+ export function register(program: Command): void {
323
+ const knowledgeCmd = program
324
+ .command("knowledge")
325
+ .description("Semantic search and indexing for docs and specs");
326
+
327
+ // Create subcommand for each index (docs, specs)
328
+ for (const indexName of KnowledgeService.getIndexNames()) {
329
+ const indexCmd = knowledgeCmd
330
+ .command(indexName)
331
+ .description(`${INDEX_CONFIGS[indexName].description} operations`);
332
+
333
+ // Search command
334
+ const searchCmd = new SearchCommand(indexName as IndexName);
335
+ const searchSubCmd = indexCmd.command(searchCmd.name).description(searchCmd.description);
336
+ searchCmd.defineArguments(searchSubCmd);
337
+ searchSubCmd.action(tracedAction(`knowledge ${indexName} search`, async (...args) => {
338
+ const opts = args[args.length - 2] as Record<string, unknown>;
339
+ const cmdObj = args[args.length - 1] as Command;
340
+ const positionalArgs = cmdObj.args;
341
+ const namedArgs: Record<string, unknown> = { ...opts };
342
+ if (positionalArgs[0]) namedArgs.query = positionalArgs[0];
343
+ const result = await searchCmd.execute(namedArgs);
344
+ console.log(JSON.stringify(result, null, 2));
345
+ // Exit explicitly - ONNX runtime thread pools don't auto-cleanup
346
+ process.exit(0);
347
+ }));
348
+
349
+ // Reindex command
350
+ const reindexCmd = new ReindexCommand(indexName as IndexName);
351
+ const reindexSubCmd = indexCmd.command(reindexCmd.name).description(reindexCmd.description);
352
+ reindexCmd.defineArguments(reindexSubCmd);
353
+ reindexSubCmd.action(tracedAction(`knowledge ${indexName} reindex`, async (...args) => {
354
+ const opts = args[args.length - 2] as Record<string, unknown>;
355
+ const result = await reindexCmd.execute(opts);
356
+ console.log(JSON.stringify(result, null, 2));
357
+ // Exit explicitly - ONNX runtime thread pools don't auto-cleanup
358
+ process.exit(0);
359
+ }));
360
+ }
361
+
362
+ // Global reindex command
363
+ const globalReindexCmd = new ReindexCommand("all");
364
+ const globalReindexSubCmd = knowledgeCmd.command("reindex").description(globalReindexCmd.description);
365
+ globalReindexCmd.defineArguments(globalReindexSubCmd);
366
+ globalReindexSubCmd.action(tracedAction('knowledge reindex', async (...args) => {
367
+ const opts = args[args.length - 2] as Record<string, unknown>;
368
+ const result = await globalReindexCmd.execute(opts);
369
+ console.log(JSON.stringify(result, null, 2));
370
+ // Exit explicitly - ONNX runtime thread pools don't auto-cleanup
371
+ process.exit(0);
372
+ }));
373
+
374
+ // Status command
375
+ const statusCmd = new StatusCommand();
376
+ const statusSubCmd = knowledgeCmd.command(statusCmd.name).description(statusCmd.description);
377
+ statusCmd.defineArguments(statusSubCmd);
378
+ statusSubCmd.action(tracedAction('knowledge status', async () => {
379
+ const result = await statusCmd.execute({});
380
+ console.log(JSON.stringify(result, null, 2));
381
+ }));
382
+ }
@@ -0,0 +1,302 @@
1
+ /**
2
+ * Memories Command (Agent-Facing)
3
+ *
4
+ * Keyword-based search for memories in docs/memories.md.
5
+ * Parses markdown tables and searches across name, domain, source, description fields.
6
+ *
7
+ * Usage:
8
+ * ah memories search <query> Search memories by keywords
9
+ * ah memories search <query> --domain planning Filter by domain
10
+ * ah memories search <query> --source user-steering Filter by source
11
+ * ah memories list List memory sections with counts
12
+ */
13
+
14
+ import { Command } from 'commander';
15
+ import { existsSync, readFileSync } from 'fs';
16
+ import { join } from 'path';
17
+ import { tracedAction } from '../lib/base-command.js';
18
+
19
+ const getProjectRoot = (): string => {
20
+ return process.env.PROJECT_ROOT || process.cwd();
21
+ };
22
+
23
+ const getMemoriesPath = (): string => {
24
+ return join(getProjectRoot(), 'docs', 'memories.md');
25
+ };
26
+
27
+ interface MemoryEntry {
28
+ name: string;
29
+ domain: string;
30
+ source: string;
31
+ description: string;
32
+ specSection: string;
33
+ }
34
+
35
+ interface MemoryMatch extends MemoryEntry {
36
+ score: number;
37
+ matchedFields: string[];
38
+ }
39
+
40
+ interface MemorySection {
41
+ name: string;
42
+ count: number;
43
+ }
44
+
45
+ /**
46
+ * Extract keywords from a search query.
47
+ * Handles quoted phrases and splits on whitespace.
48
+ */
49
+ function extractKeywords(query: string): string[] {
50
+ const keywords: string[] = [];
51
+
52
+ // Extract quoted phrases first
53
+ const quotedRegex = /"([^"]+)"/g;
54
+ let match;
55
+ while ((match = quotedRegex.exec(query)) !== null) {
56
+ keywords.push(match[1].toLowerCase());
57
+ }
58
+
59
+ // Remove quoted phrases and split remaining on whitespace
60
+ const remaining = query.replace(quotedRegex, '').trim();
61
+ if (remaining) {
62
+ keywords.push(...remaining.toLowerCase().split(/\s+/).filter(k => k.length > 0));
63
+ }
64
+
65
+ return keywords;
66
+ }
67
+
68
+ /**
69
+ * Parse markdown tables from memories.md into structured entries.
70
+ * Expects tables with columns: Name, Domain, Source, Description
71
+ * grouped under ## section headers.
72
+ */
73
+ function parseMemories(filePath: string): MemoryEntry[] {
74
+ if (!existsSync(filePath)) return [];
75
+
76
+ const content = readFileSync(filePath, 'utf-8');
77
+ const lines = content.split('\n');
78
+ const entries: MemoryEntry[] = [];
79
+ let currentSection = 'unknown';
80
+
81
+ for (let i = 0; i < lines.length; i++) {
82
+ const line = lines[i].trim();
83
+
84
+ // Track section headers
85
+ if (line.startsWith('## ')) {
86
+ currentSection = line.replace('## ', '').trim();
87
+ continue;
88
+ }
89
+
90
+ // Skip non-table lines, header rows, and separator rows
91
+ if (!line.startsWith('|') || line.includes('---') || /\|\s*Name\s*\|/i.test(line)) {
92
+ continue;
93
+ }
94
+
95
+ // Parse table row
96
+ const cells = line.split('|').map(c => c.trim()).filter(c => c.length > 0);
97
+ if (cells.length >= 4) {
98
+ entries.push({
99
+ name: cells[0],
100
+ domain: cells[1],
101
+ source: cells[2],
102
+ description: cells[3],
103
+ specSection: currentSection,
104
+ });
105
+ }
106
+ }
107
+
108
+ return entries;
109
+ }
110
+
111
+ /**
112
+ * Get section names and entry counts from memories file.
113
+ */
114
+ function getMemorySections(filePath: string): MemorySection[] {
115
+ if (!existsSync(filePath)) return [];
116
+
117
+ const content = readFileSync(filePath, 'utf-8');
118
+ const lines = content.split('\n');
119
+ const sections: MemorySection[] = [];
120
+ let currentSection = '';
121
+ let currentCount = 0;
122
+
123
+ for (const line of lines) {
124
+ const trimmed = line.trim();
125
+
126
+ if (trimmed.startsWith('## ')) {
127
+ if (currentSection) {
128
+ sections.push({ name: currentSection, count: currentCount });
129
+ }
130
+ currentSection = trimmed.replace('## ', '').trim();
131
+ currentCount = 0;
132
+ continue;
133
+ }
134
+
135
+ // Count data rows (not headers or separators)
136
+ if (currentSection && trimmed.startsWith('|') && !trimmed.includes('---') && !/\|\s*Name\s*\|/i.test(trimmed)) {
137
+ const cells = trimmed.split('|').map(c => c.trim()).filter(c => c.length > 0);
138
+ if (cells.length >= 4) {
139
+ currentCount++;
140
+ }
141
+ }
142
+ }
143
+
144
+ // Push final section
145
+ if (currentSection) {
146
+ sections.push({ name: currentSection, count: currentCount });
147
+ }
148
+
149
+ return sections;
150
+ }
151
+
152
+ /**
153
+ * Score how well a memory matches the search keywords.
154
+ */
155
+ function scoreMemory(entry: MemoryEntry, keywords: string[]): { score: number; matchedFields: string[] } {
156
+ let score = 0;
157
+ const matchedFields: string[] = [];
158
+
159
+ for (const keyword of keywords) {
160
+ // Name match (high weight)
161
+ if (entry.name.toLowerCase().includes(keyword)) {
162
+ score += 3;
163
+ if (!matchedFields.includes('name')) matchedFields.push('name');
164
+ }
165
+
166
+ // Description match (medium weight)
167
+ if (entry.description.toLowerCase().includes(keyword)) {
168
+ score += 2;
169
+ if (!matchedFields.includes('description')) matchedFields.push('description');
170
+ }
171
+
172
+ // Domain match (medium weight)
173
+ if (entry.domain.toLowerCase().includes(keyword)) {
174
+ score += 2;
175
+ if (!matchedFields.includes('domain')) matchedFields.push('domain');
176
+ }
177
+
178
+ // Source match (low weight)
179
+ if (entry.source.toLowerCase().includes(keyword)) {
180
+ score += 1;
181
+ if (!matchedFields.includes('source')) matchedFields.push('source');
182
+ }
183
+ }
184
+
185
+ return { score, matchedFields };
186
+ }
187
+
188
+ /**
189
+ * Search memories matching a query with optional filters.
190
+ */
191
+ function searchMemories(
192
+ query: string,
193
+ options: { domain?: string; source?: string; limit?: number }
194
+ ): MemoryMatch[] {
195
+ const memoriesPath = getMemoriesPath();
196
+ const keywords = extractKeywords(query);
197
+ const limit = options.limit ?? 10;
198
+
199
+ if (keywords.length === 0) return [];
200
+
201
+ let entries = parseMemories(memoriesPath);
202
+
203
+ // Apply filters
204
+ if (options.domain) {
205
+ entries = entries.filter(e => e.domain.toLowerCase() === options.domain!.toLowerCase());
206
+ }
207
+ if (options.source) {
208
+ entries = entries.filter(e => e.source.toLowerCase() === options.source!.toLowerCase());
209
+ }
210
+
211
+ const matches: MemoryMatch[] = [];
212
+
213
+ for (const entry of entries) {
214
+ const { score, matchedFields } = scoreMemory(entry, keywords);
215
+ if (score > 0) {
216
+ matches.push({ ...entry, score, matchedFields });
217
+ }
218
+ }
219
+
220
+ // Sort by score descending
221
+ matches.sort((a, b) => b.score - a.score);
222
+
223
+ return matches.slice(0, limit);
224
+ }
225
+
226
+ export function register(program: Command): void {
227
+ const memoriesCmd = program
228
+ .command('memories')
229
+ .description('Search and browse project memories');
230
+
231
+ // Search command
232
+ memoriesCmd
233
+ .command('search <query>')
234
+ .description('Search memories by keywords (searches name, domain, source, description)')
235
+ .option('--domain <domain>', 'Filter by domain (planning, validation, implementation, harness-tooling, ideation)')
236
+ .option('--source <source>', 'Filter by source (user-steering, agent-inferred)')
237
+ .option('--limit <n>', 'Maximum number of results', '10')
238
+ .action(tracedAction('memories search', async (query: string, options: { domain?: string; source?: string; limit?: string }) => {
239
+ const limit = parseInt(options.limit || '10', 10);
240
+ const matches = searchMemories(query, {
241
+ domain: options.domain,
242
+ source: options.source,
243
+ limit,
244
+ });
245
+
246
+ if (matches.length === 0) {
247
+ console.log(JSON.stringify({
248
+ success: true,
249
+ query,
250
+ keywords: extractKeywords(query),
251
+ results: [],
252
+ message: 'No matching memories found',
253
+ }, null, 2));
254
+ return;
255
+ }
256
+
257
+ const results = matches.map(match => ({
258
+ name: match.name,
259
+ domain: match.domain,
260
+ source: match.source,
261
+ description: match.description,
262
+ spec_section: match.specSection,
263
+ score: match.score,
264
+ matched_fields: match.matchedFields,
265
+ }));
266
+
267
+ console.log(JSON.stringify({
268
+ success: true,
269
+ query,
270
+ keywords: extractKeywords(query),
271
+ result_count: results.length,
272
+ results,
273
+ }, null, 2));
274
+ }));
275
+
276
+ // List command
277
+ memoriesCmd
278
+ .command('list')
279
+ .description('List memory sections with entry counts')
280
+ .action(tracedAction('memories list', async () => {
281
+ const memoriesPath = getMemoriesPath();
282
+
283
+ if (!existsSync(memoriesPath)) {
284
+ console.log(JSON.stringify({
285
+ success: true,
286
+ message: 'No memories file found. Create docs/memories.md to start capturing learnings.',
287
+ sections: [],
288
+ total_entries: 0,
289
+ }, null, 2));
290
+ return;
291
+ }
292
+
293
+ const sections = getMemorySections(memoriesPath);
294
+ const totalEntries = sections.reduce((sum, s) => sum + s.count, 0);
295
+
296
+ console.log(JSON.stringify({
297
+ success: true,
298
+ sections,
299
+ total_entries: totalEntries,
300
+ }, null, 2));
301
+ }));
302
+ }