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,557 @@
1
+ /**
2
+ * Documentation commands - validation and reference finalization.
3
+ *
4
+ * Uses ctags for symbol lookup (instead of AST parsing) for broader language support
5
+ * and simpler implementation.
6
+ *
7
+ * Commands:
8
+ * ah docs validate [--path <path>] - Validate all refs in docs/
9
+ * ah docs finalize [--path <path>] - Finalize placeholder refs with hashes
10
+ * ah docs tree <path> - Get tree with doc coverage
11
+ */
12
+
13
+ import { Command } from "commander";
14
+ import { existsSync, readdirSync, readFileSync, statSync, writeFileSync } from "fs";
15
+ import { dirname, extname, join, relative } from "path";
16
+ import {
17
+ addCommonOptions,
18
+ CommandResult,
19
+ executeCommand,
20
+ parseContext,
21
+ } from "../lib/base-command.js";
22
+ import {
23
+ checkCtagsAvailable,
24
+ findSymbolInFile,
25
+ generateCtagsIndex,
26
+ } from "../lib/ctags.js";
27
+ import {
28
+ batchGetBlobHashes,
29
+ findMarkdownFiles,
30
+ isCodeFile,
31
+ validateDocsAsync,
32
+ } from "../lib/docs-validation.js";
33
+ import { getProjectRoot } from "../lib/git.js";
34
+
35
+ /**
36
+ * Validate all documentation references.
37
+ */
38
+ /** Paths excluded from validation and finalization (relative to project root). */
39
+ const EXCLUDED_DOC_PATHS = ["docs/memories.md", "docs/solutions"];
40
+
41
+ async function validate(docsPath: string, options?: { useCache?: boolean }): Promise<CommandResult> {
42
+ const projectRoot = getProjectRoot();
43
+ const absoluteDocsPath = docsPath.startsWith("/")
44
+ ? docsPath
45
+ : join(projectRoot, docsPath);
46
+
47
+ // Check ctags availability first
48
+ const ctagsCheck = checkCtagsAvailable();
49
+ if (!ctagsCheck.available) {
50
+ return {
51
+ success: false,
52
+ error: `ctags_unavailable: ${ctagsCheck.error}`,
53
+ };
54
+ }
55
+
56
+ const excludePaths = EXCLUDED_DOC_PATHS.map((p) => join(projectRoot, p));
57
+
58
+ // Run validation (with optional caching)
59
+ const result = await validateDocsAsync(absoluteDocsPath, projectRoot, {
60
+ useCache: options?.useCache ?? false,
61
+ excludePaths,
62
+ });
63
+
64
+ // Consider it a success even with issues (issues are in the data)
65
+ return { success: true, data: result };
66
+ }
67
+
68
+ /**
69
+ * Get tree structure with documentation coverage.
70
+ */
71
+ async function tree(pathArg: string, maxDepth: number): Promise<CommandResult> {
72
+ const projectRoot = getProjectRoot();
73
+ const absolutePath = pathArg.startsWith("/")
74
+ ? pathArg
75
+ : join(projectRoot, pathArg);
76
+ const relativePath = relative(projectRoot, absolutePath);
77
+
78
+ if (!existsSync(absolutePath)) {
79
+ return {
80
+ success: false,
81
+ error: `path_not_found: Path not found: ${relativePath}`,
82
+ };
83
+ }
84
+
85
+ const stat = statSync(absolutePath);
86
+ if (!stat.isDirectory()) {
87
+ return {
88
+ success: false,
89
+ error: `not_directory: ${relativePath} is not a directory`,
90
+ };
91
+ }
92
+
93
+ const docsPath = join(projectRoot, "docs");
94
+
95
+ interface TreeNode {
96
+ name: string;
97
+ type: "file" | "directory";
98
+ has_docs: boolean;
99
+ doc_path?: string;
100
+ children?: TreeNode[];
101
+ }
102
+
103
+ const sourceExtensions = [".ts", ".tsx", ".js", ".jsx", ".py", ".go", ".rs", ".java", ".rb"];
104
+
105
+ const buildTree = (dir: string, depth: number): TreeNode[] => {
106
+ if (depth <= 0) return [];
107
+
108
+ const entries = readdirSync(dir);
109
+ const nodes: TreeNode[] = [];
110
+
111
+ for (const entry of entries) {
112
+ if (entry.startsWith(".") || entry === "node_modules") continue;
113
+
114
+ const fullPath = join(dir, entry);
115
+ const entryRelPath = relative(projectRoot, fullPath);
116
+ const entryStat = statSync(fullPath);
117
+
118
+ // Check for docs coverage
119
+ const possibleDocPaths = [
120
+ join(docsPath, entryRelPath + ".md"),
121
+ join(docsPath, dirname(entryRelPath), entry.replace(extname(entry), ".md")),
122
+ join(docsPath, entryRelPath, "index.md"),
123
+ ];
124
+
125
+ let hasDoc = false;
126
+ let docPath: string | undefined;
127
+ for (const dp of possibleDocPaths) {
128
+ if (existsSync(dp)) {
129
+ hasDoc = true;
130
+ docPath = relative(projectRoot, dp);
131
+ break;
132
+ }
133
+ }
134
+
135
+ if (entryStat.isDirectory()) {
136
+ const children = buildTree(fullPath, depth - 1);
137
+ nodes.push({
138
+ name: entry,
139
+ type: "directory",
140
+ has_docs: hasDoc,
141
+ doc_path: docPath,
142
+ children: children.length > 0 ? children : undefined,
143
+ });
144
+ } else {
145
+ const ext = extname(entry);
146
+ if (sourceExtensions.includes(ext)) {
147
+ nodes.push({
148
+ name: entry,
149
+ type: "file",
150
+ has_docs: hasDoc,
151
+ doc_path: docPath,
152
+ });
153
+ }
154
+ }
155
+ }
156
+
157
+ return nodes;
158
+ };
159
+
160
+ const treeData = buildTree(absolutePath, maxDepth);
161
+
162
+ // Calculate coverage stats
163
+ const countNodes = (nodes: TreeNode[]): { total: number; covered: number } => {
164
+ let total = 0;
165
+ let covered = 0;
166
+ for (const node of nodes) {
167
+ total++;
168
+ if (node.has_docs) covered++;
169
+ if (node.children) {
170
+ const childStats = countNodes(node.children);
171
+ total += childStats.total;
172
+ covered += childStats.covered;
173
+ }
174
+ }
175
+ return { total, covered };
176
+ };
177
+
178
+ const stats = countNodes(treeData);
179
+
180
+ return {
181
+ success: true,
182
+ data: {
183
+ path: relativePath,
184
+ tree: treeData,
185
+ coverage: {
186
+ total: stats.total,
187
+ covered: stats.covered,
188
+ percentage: stats.total > 0 ? Math.round((stats.covered / stats.total) * 100) : 0,
189
+ },
190
+ },
191
+ };
192
+ }
193
+
194
+ /**
195
+ * Placeholder ref pattern - matches [ref:file:symbol] without hash
196
+ */
197
+ // Matches both [ref:file:symbol] and [ref:file] (file-only refs)
198
+ const PLACEHOLDER_REF_PATTERN = /\[ref:([^:\]]+)(?::([^\]]*))?\]/g;
199
+
200
+ /**
201
+ * Finalize a single documentation file by replacing placeholder refs with full refs.
202
+ */
203
+ function finalizeSingleFile(
204
+ absolutePath: string,
205
+ projectRoot: string,
206
+ hashCache: Map<string, { success: boolean; hash?: string; error?: string }>
207
+ ): { path: string; replacements: number; replaced: Array<{ from: string; to: string }>; errors: Array<{ placeholder: string; reason: string }> } {
208
+ const relativePath = relative(projectRoot, absolutePath);
209
+ const content = readFileSync(absolutePath, "utf-8");
210
+
211
+ // Find all placeholder refs (symbol may be undefined for file-only refs)
212
+ const placeholders: Array<{ match: string; file: string; symbol: string | undefined }> = [];
213
+ let match;
214
+ const pattern = new RegExp(PLACEHOLDER_REF_PATTERN.source, "g");
215
+ while ((match = pattern.exec(content)) !== null) {
216
+ // Skip if it already looks like a full ref (has 3 colons indicating hash)
217
+ if (match[0].split(":").length > 3) continue;
218
+ placeholders.push({
219
+ match: match[0],
220
+ file: match[1],
221
+ symbol: match[2],
222
+ });
223
+ }
224
+
225
+ if (placeholders.length === 0) {
226
+ return { path: relativePath, replacements: 0, replaced: [], errors: [] };
227
+ }
228
+
229
+ // Process each placeholder
230
+ const errors: Array<{ placeholder: string; reason: string }> = [];
231
+ const replacements: Array<{ from: string; to: string }> = [];
232
+ let finalizedContent = content;
233
+
234
+ for (const placeholder of placeholders) {
235
+ const absoluteFilePath = join(projectRoot, placeholder.file);
236
+
237
+ // Check file exists
238
+ if (!existsSync(absoluteFilePath)) {
239
+ errors.push({
240
+ placeholder: placeholder.match,
241
+ reason: `File not found: ${placeholder.file}`,
242
+ });
243
+ continue;
244
+ }
245
+
246
+ // Get hash from cache
247
+ const hashResult = hashCache.get(absoluteFilePath) || hashCache.get(placeholder.file);
248
+ if (!hashResult || !hashResult.success) {
249
+ errors.push({
250
+ placeholder: placeholder.match,
251
+ reason: `Git hash lookup failed for ${placeholder.file} (uncommitted file?)`,
252
+ });
253
+ continue;
254
+ }
255
+
256
+ // For file-only refs (empty symbol), just add the hash
257
+ if (!placeholder.symbol || placeholder.symbol.trim() === "") {
258
+ const fullRef = `[ref:${placeholder.file}::${hashResult.hash}]`;
259
+ finalizedContent = finalizedContent.replaceAll(placeholder.match, fullRef);
260
+ replacements.push({ from: placeholder.match, to: fullRef });
261
+ continue;
262
+ }
263
+
264
+ // For non-code files (markdown, yaml, json, etc.), treat symbol as a label (no ctags lookup)
265
+ if (!isCodeFile(placeholder.file)) {
266
+ const fullRef = `[ref:${placeholder.file}:${placeholder.symbol}:${hashResult.hash}]`;
267
+ finalizedContent = finalizedContent.replaceAll(placeholder.match, fullRef);
268
+ replacements.push({ from: placeholder.match, to: fullRef });
269
+ continue;
270
+ }
271
+
272
+ // For code files with symbol refs, verify symbol exists via ctags
273
+ const entry = findSymbolInFile(absoluteFilePath, placeholder.symbol, projectRoot);
274
+ if (!entry) {
275
+ errors.push({
276
+ placeholder: placeholder.match,
277
+ reason: `Symbol '${placeholder.symbol}' not found in ${placeholder.file}`,
278
+ });
279
+ continue;
280
+ }
281
+
282
+ // Create full ref
283
+ const fullRef = `[ref:${placeholder.file}:${placeholder.symbol}:${hashResult.hash}]`;
284
+ finalizedContent = finalizedContent.replaceAll(placeholder.match, fullRef);
285
+ replacements.push({ from: placeholder.match, to: fullRef });
286
+ }
287
+
288
+ // Write back if there were successful replacements
289
+ if (replacements.length > 0) {
290
+ writeFileSync(absolutePath, finalizedContent, "utf-8");
291
+ }
292
+
293
+ return { path: relativePath, replacements: replacements.length, replaced: replacements, errors };
294
+ }
295
+
296
+ /**
297
+ * Refresh a single documentation file by updating all finalized ref hashes to current blob hashes.
298
+ */
299
+ function refreshSingleFile(
300
+ absolutePath: string,
301
+ projectRoot: string,
302
+ hashCache: Map<string, { hash: string; success: boolean }>
303
+ ): { path: string; updated: number; unchanged: number; errors: number } {
304
+ const relativePath = relative(projectRoot, absolutePath);
305
+ const content = readFileSync(absolutePath, "utf-8");
306
+
307
+ // Find all finalized refs matching REF_PATTERN
308
+ const pattern = new RegExp(REF_PATTERN.source, "g");
309
+ const matches: Array<{ full: string; file: string; symbol: string; hash: string }> = [];
310
+ let match;
311
+ while ((match = pattern.exec(content)) !== null) {
312
+ matches.push({ full: match[0], file: match[1], symbol: match[2], hash: match[3] });
313
+ }
314
+
315
+ if (matches.length === 0) {
316
+ return { path: relativePath, updated: 0, unchanged: 0, errors: 0 };
317
+ }
318
+
319
+ let updated = 0;
320
+ let unchanged = 0;
321
+ let errors = 0;
322
+ let refreshedContent = content;
323
+
324
+ for (const m of matches) {
325
+ const absoluteFilePath = join(projectRoot, m.file);
326
+ const hashResult = hashCache.get(absoluteFilePath) || hashCache.get(m.file);
327
+ if (!hashResult || !hashResult.success) {
328
+ errors++;
329
+ continue;
330
+ }
331
+
332
+ if (m.hash === hashResult.hash) {
333
+ unchanged++;
334
+ continue;
335
+ }
336
+
337
+ // Replace old hash with current blob hash
338
+ const oldRef = m.full;
339
+ const newRef = m.symbol === ""
340
+ ? `[ref:${m.file}::${hashResult.hash}]`
341
+ : `[ref:${m.file}:${m.symbol}:${hashResult.hash}]`;
342
+ refreshedContent = refreshedContent.replaceAll(oldRef, newRef);
343
+ updated++;
344
+ }
345
+
346
+ if (updated > 0) {
347
+ writeFileSync(absolutePath, refreshedContent, "utf-8");
348
+ }
349
+
350
+ return { path: relativePath, updated, unchanged, errors };
351
+ }
352
+
353
+ /**
354
+ * Finalize documentation files by replacing placeholder refs with full refs.
355
+ * Supports both single file and directory (batch) operation.
356
+ * This allows writers to use [ref:file:symbol] syntax without hashes during writing,
357
+ * then batch-process all refs in a single pass.
358
+ *
359
+ * When refresh=true, operates on ALL finalized refs (not just placeholders),
360
+ * replacing stored hashes with current blob hashes.
361
+ */
362
+ async function finalize(docsPath: string, options?: { refresh?: boolean }): Promise<CommandResult> {
363
+ const projectRoot = getProjectRoot();
364
+ const absolutePath = docsPath.startsWith("/") ? docsPath : join(projectRoot, docsPath);
365
+ const relativePath = relative(projectRoot, absolutePath);
366
+
367
+ if (!existsSync(absolutePath)) {
368
+ return {
369
+ success: false,
370
+ error: `path_not_found: Path not found: ${relativePath}`,
371
+ };
372
+ }
373
+
374
+ // Check ctags availability
375
+ const ctagsCheck = checkCtagsAvailable();
376
+ if (!ctagsCheck.available) {
377
+ return {
378
+ success: false,
379
+ error: `ctags_unavailable: ${ctagsCheck.error}`,
380
+ };
381
+ }
382
+
383
+ // Generate ctags index for symbol lookup
384
+ generateCtagsIndex(projectRoot);
385
+
386
+ // Determine if path is a file or directory
387
+ const excludePaths = EXCLUDED_DOC_PATHS.map((p) => join(projectRoot, p));
388
+ const stat = statSync(absolutePath);
389
+ const filesToProcess: string[] = stat.isDirectory()
390
+ ? findMarkdownFiles(absolutePath, false, excludePaths)
391
+ : excludePaths.some((ep) => absolutePath === ep || absolutePath.startsWith(ep + "/"))
392
+ ? []
393
+ : [absolutePath];
394
+
395
+ if (filesToProcess.length === 0) {
396
+ return {
397
+ success: true,
398
+ data: {
399
+ message: "No markdown files found",
400
+ path: relativePath,
401
+ filesProcessed: 0,
402
+ totalReplacements: 0,
403
+ },
404
+ };
405
+ }
406
+
407
+ // --refresh mode: update all finalized ref hashes to current blob hashes
408
+ if (options?.refresh) {
409
+ // Collect all referenced files from finalized refs across all files
410
+ const allRefFiles: Set<string> = new Set();
411
+ for (const docFile of filesToProcess) {
412
+ const content = readFileSync(docFile, "utf-8");
413
+ const refPattern = new RegExp(REF_PATTERN.source, "g");
414
+ let match;
415
+ while ((match = refPattern.exec(content)) !== null) {
416
+ allRefFiles.add(match[1]);
417
+ }
418
+ }
419
+
420
+ // Batch get blob hashes for all referenced files
421
+ const absoluteRefFiles = [...allRefFiles].map((f) => join(projectRoot, f));
422
+ const refreshHashCache = batchGetBlobHashes(absoluteRefFiles, projectRoot);
423
+
424
+ // Process each file
425
+ let totalUpdated = 0;
426
+ let totalUnchanged = 0;
427
+ let totalRefreshErrors = 0;
428
+ const refreshResults: Array<{ path: string; updated: number; unchanged: number; errors: number }> = [];
429
+
430
+ for (const docFile of filesToProcess) {
431
+ const result = refreshSingleFile(docFile, projectRoot, refreshHashCache);
432
+ refreshResults.push(result);
433
+ totalUpdated += result.updated;
434
+ totalUnchanged += result.unchanged;
435
+ totalRefreshErrors += result.errors;
436
+ }
437
+
438
+ return {
439
+ success: totalRefreshErrors === 0,
440
+ error: totalRefreshErrors > 0 ? `Refresh had ${totalRefreshErrors} hash lookup error(s)` : undefined,
441
+ data: {
442
+ mode: "refresh",
443
+ path: relativePath,
444
+ filesProcessed: filesToProcess.length,
445
+ totalUpdated,
446
+ totalUnchanged,
447
+ totalErrors: totalRefreshErrors,
448
+ files: refreshResults.filter((r) => r.updated > 0 || r.errors > 0),
449
+ },
450
+ };
451
+ }
452
+
453
+ // Collect all placeholder refs across all files to batch hash lookup
454
+ const allPlaceholders: Array<{ file: string; docPath: string }> = [];
455
+ for (const docFile of filesToProcess) {
456
+ const content = readFileSync(docFile, "utf-8");
457
+ const pattern = new RegExp(PLACEHOLDER_REF_PATTERN.source, "g");
458
+ let match;
459
+ while ((match = pattern.exec(content)) !== null) {
460
+ if (match[0].split(":").length > 3) continue;
461
+ allPlaceholders.push({ file: match[1], docPath: docFile });
462
+ }
463
+ }
464
+
465
+ // Batch get file hashes for all referenced files
466
+ const uniqueFiles = [...new Set(allPlaceholders.map((p) => p.file))];
467
+ const absoluteFiles = uniqueFiles.map((f) => join(projectRoot, f));
468
+ const hashCache = batchGetBlobHashes(absoluteFiles, projectRoot);
469
+
470
+ // Process each file
471
+ const results: Array<{ path: string; replacements: number; errors: number }> = [];
472
+ let totalReplacements = 0;
473
+ let totalErrors = 0;
474
+ const allErrors: Array<{ file: string; placeholder: string; reason: string }> = [];
475
+
476
+ for (const docFile of filesToProcess) {
477
+ const result = finalizeSingleFile(docFile, projectRoot, hashCache);
478
+ results.push({
479
+ path: result.path,
480
+ replacements: result.replacements,
481
+ errors: result.errors.length,
482
+ });
483
+ totalReplacements += result.replacements;
484
+ totalErrors += result.errors.length;
485
+ for (const err of result.errors) {
486
+ allErrors.push({ file: result.path, ...err });
487
+ }
488
+ }
489
+
490
+ const hasErrors = totalErrors > 0;
491
+ return {
492
+ success: !hasErrors,
493
+ error: hasErrors ? `Finalization had ${totalErrors} error(s) in ${allErrors.length} file(s)` : undefined,
494
+ data: {
495
+ path: relativePath,
496
+ filesProcessed: filesToProcess.length,
497
+ totalReplacements,
498
+ totalErrors,
499
+ files: results.filter((r) => r.replacements > 0 || r.errors > 0),
500
+ errors: allErrors.length > 0 ? allErrors : undefined,
501
+ },
502
+ };
503
+ }
504
+
505
+ /**
506
+ * Register docs commands.
507
+ */
508
+ export function register(program: Command): void {
509
+ const docs = program.command("docs").description("Documentation management and validation");
510
+
511
+ // validate
512
+ const validateCmd = docs
513
+ .command("validate")
514
+ .description("Validate all documentation references")
515
+ .option("--path <path>", "Docs directory path", "docs/")
516
+ .option("--cache", "Use validation cache for faster repeated runs", false);
517
+
518
+ addCommonOptions(validateCmd);
519
+
520
+ validateCmd.action(async (options) => {
521
+ const context = parseContext(options);
522
+ await executeCommand("docs:validate", context, () =>
523
+ validate(options.path, { useCache: options.cache })
524
+ );
525
+ });
526
+
527
+ // tree
528
+ const treeCmd = docs
529
+ .command("tree")
530
+ .description("Get tree structure with documentation coverage")
531
+ .argument("<path>", "Directory path")
532
+ .option("--depth <n>", "Max depth to traverse", "3");
533
+
534
+ addCommonOptions(treeCmd);
535
+
536
+ treeCmd.action(async (path: string, options) => {
537
+ const context = parseContext(options);
538
+ const depth = parseInt(options.depth || "3", 10);
539
+ await executeCommand("docs:tree", context, () => tree(path, depth));
540
+ });
541
+
542
+ // finalize
543
+ const finalizeCmd = docs
544
+ .command("finalize")
545
+ .description("Replace placeholder refs [ref:file:symbol] with full refs including hashes")
546
+ .option("--path <path>", "Docs path (file or directory)", "docs/")
547
+ .option("--refresh", "Re-stamp all finalized refs with current blob hashes", false);
548
+
549
+ addCommonOptions(finalizeCmd);
550
+
551
+ finalizeCmd.action(async (options) => {
552
+ const context = parseContext(options);
553
+ await executeCommand("docs:finalize", context, () =>
554
+ finalize(options.path, { refresh: options.refresh })
555
+ );
556
+ });
557
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Hooks Command (Hidden)
3
+ *
4
+ * Internal command for Claude Code hook handlers.
5
+ * All hooks read JSON from stdin and output JSON to stdout.
6
+ *
7
+ * Subcommands are auto-discovered from src/hooks/*.ts files.
8
+ * Each hook module exports a `register(parent: Command)` function.
9
+ *
10
+ * Usage:
11
+ * echo '{"tool_name":"WebFetch","tool_input":{"url":"..."}}' | ah hooks enforcement github-url
12
+ */
13
+
14
+ import { Command } from 'commander';
15
+ import { discoverAndRegisterHooks } from '../hooks/index.js';
16
+
17
+ export function register(program: Command): void {
18
+ const hooks = program
19
+ .command('hooks', { hidden: true })
20
+ .description('Hook commands (internal use)');
21
+
22
+ // Auto-discover and register hook subcommands
23
+ discoverAndRegisterHooks(hooks);
24
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Command Registry - Auto-discovers command modules.
3
+ *
4
+ * Each command file should export a `register` function that takes
5
+ * a Commander program and registers its commands.
6
+ */
7
+
8
+ import { readdirSync, statSync } from 'fs';
9
+ import { dirname, join } from 'path';
10
+ import { fileURLToPath } from 'url';
11
+ import type { Command } from 'commander';
12
+
13
+ const __dirname = dirname(fileURLToPath(import.meta.url));
14
+
15
+ export interface CommandModule {
16
+ register: (program: Command) => void;
17
+ }
18
+
19
+ /**
20
+ * Auto-discover and register all command modules in the commands directory.
21
+ *
22
+ * Discovers:
23
+ * - Single-file modules: foo.ts exports { register }
24
+ * - Skips: index.ts, base files
25
+ */
26
+ export async function discoverAndRegister(program: Command): Promise<void> {
27
+ const entries = readdirSync(__dirname);
28
+
29
+ for (const entry of entries) {
30
+ const entryPath = join(__dirname, entry);
31
+ const stat = statSync(entryPath);
32
+
33
+ // Skip directories and non-ts files
34
+ if (stat.isDirectory()) continue;
35
+ if (!entry.endsWith('.ts')) continue;
36
+ if (entry === 'index.ts') continue;
37
+
38
+ const moduleName = entry.replace('.ts', '');
39
+ const importPath = `./${moduleName}.js`;
40
+
41
+ try {
42
+ const module = (await import(importPath)) as CommandModule;
43
+ if (typeof module.register === 'function') {
44
+ module.register(program);
45
+ }
46
+ } catch (e) {
47
+ // Skip modules with errors - log for debugging
48
+ console.error(`Warning: Could not load ${moduleName}: ${e}`);
49
+ }
50
+ }
51
+ }