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,318 @@
1
+ /**
2
+ * Custom Assertion Helpers for Harness Testing
3
+ *
4
+ * Provides domain-specific assertions for CLI and hook testing.
5
+ */
6
+
7
+ import { expect } from 'vitest';
8
+ import type { RunResult } from './cli-runner.js';
9
+ import type { HookResult } from './hook-runner.js';
10
+ import type { TestFixture } from './fixture.js';
11
+ import { readFileSync, existsSync } from 'fs';
12
+ import { join } from 'path';
13
+
14
+ // ─────────────────────────────────────────────────────────────────────────────
15
+ // CLI Result Assertions
16
+ // ─────────────────────────────────────────────────────────────────────────────
17
+
18
+ /**
19
+ * Assert that a CLI command succeeded.
20
+ */
21
+ export function assertSuccess(result: RunResult, message?: string): void {
22
+ expect(result.success, message ?? `Expected success but got exit code ${result.exitCode}: ${result.stderr}`).toBe(true);
23
+ }
24
+
25
+ /**
26
+ * Assert that a CLI command failed.
27
+ */
28
+ export function assertFailure(result: RunResult, message?: string): void {
29
+ expect(result.success, message ?? `Expected failure but command succeeded`).toBe(false);
30
+ }
31
+
32
+ /**
33
+ * Assert stdout contains expected text.
34
+ */
35
+ export function assertStdoutContains(result: RunResult, expected: string, message?: string): void {
36
+ expect(
37
+ result.stdout.includes(expected),
38
+ message ?? `Expected stdout to contain "${expected}"\nActual stdout:\n${result.stdout}`
39
+ ).toBe(true);
40
+ }
41
+
42
+ /**
43
+ * Assert stderr contains expected text.
44
+ */
45
+ export function assertStderrContains(result: RunResult, expected: string, message?: string): void {
46
+ expect(
47
+ result.stderr.includes(expected),
48
+ message ?? `Expected stderr to contain "${expected}"\nActual stderr:\n${result.stderr}`
49
+ ).toBe(true);
50
+ }
51
+
52
+ /**
53
+ * Assert stdout matches a regex.
54
+ */
55
+ export function assertStdoutMatches(result: RunResult, pattern: RegExp, message?: string): void {
56
+ expect(
57
+ pattern.test(result.stdout),
58
+ message ?? `Expected stdout to match ${pattern}\nActual stdout:\n${result.stdout}`
59
+ ).toBe(true);
60
+ }
61
+
62
+ /**
63
+ * Assert JSON output has expected structure.
64
+ */
65
+ export function assertJsonOutput<T>(
66
+ result: RunResult,
67
+ validator: (json: T) => boolean,
68
+ message?: string
69
+ ): void {
70
+ expect(result.json, 'Expected JSON output but none was parsed').toBeDefined();
71
+ expect(
72
+ validator(result.json as T),
73
+ message ?? `JSON output validation failed: ${JSON.stringify(result.json, null, 2)}`
74
+ ).toBe(true);
75
+ }
76
+
77
+ /**
78
+ * Assert command completed within time limit.
79
+ */
80
+ export function assertTimedWithin(result: RunResult, maxMs: number, message?: string): void {
81
+ expect(
82
+ result.duration <= maxMs,
83
+ message ?? `Expected completion within ${maxMs}ms but took ${result.duration}ms`
84
+ ).toBe(true);
85
+ }
86
+
87
+ // ─────────────────────────────────────────────────────────────────────────────
88
+ // Hook Result Assertions
89
+ // ─────────────────────────────────────────────────────────────────────────────
90
+
91
+ /**
92
+ * Assert that a hook allowed the tool.
93
+ */
94
+ export function assertHookAllowed(result: HookResult, message?: string): void {
95
+ expect(
96
+ result.allowed,
97
+ message ?? `Expected hook to allow tool but it was ${result.denied ? 'denied' : 'blocked'}: ${result.denialReason ?? result.stderr}`
98
+ ).toBe(true);
99
+ }
100
+
101
+ /**
102
+ * Assert that a hook denied the tool.
103
+ */
104
+ export function assertHookDenied(result: HookResult, message?: string): void {
105
+ expect(
106
+ result.denied,
107
+ message ?? `Expected hook to deny tool but it was allowed`
108
+ ).toBe(true);
109
+ }
110
+
111
+ /**
112
+ * Assert that a hook blocked the tool (PostToolUse).
113
+ */
114
+ export function assertHookBlocked(result: HookResult, message?: string): void {
115
+ expect(
116
+ result.blocked,
117
+ message ?? `Expected hook to block tool but it was allowed`
118
+ ).toBe(true);
119
+ }
120
+
121
+ /**
122
+ * Assert that a hook injected context.
123
+ */
124
+ export function assertHookInjectedContext(result: HookResult, message?: string): void {
125
+ expect(
126
+ result.systemMessage,
127
+ message ?? `Expected hook to inject context (systemMessage) but none was found`
128
+ ).toBeDefined();
129
+ expect(result.systemMessage!.length).toBeGreaterThan(0);
130
+ }
131
+
132
+ /**
133
+ * Assert that hook context contains expected text.
134
+ */
135
+ export function assertHookContextContains(result: HookResult, expected: string, message?: string): void {
136
+ assertHookInjectedContext(result);
137
+ expect(
138
+ result.systemMessage!.includes(expected),
139
+ message ?? `Expected systemMessage to contain "${expected}"\nActual:\n${result.systemMessage!.substring(0, 500)}`
140
+ ).toBe(true);
141
+ }
142
+
143
+ /**
144
+ * Assert denial reason contains expected text.
145
+ */
146
+ export function assertDenialReasonContains(result: HookResult, expected: string, message?: string): void {
147
+ assertHookDenied(result);
148
+ expect(
149
+ result.denialReason?.includes(expected),
150
+ message ?? `Expected denial reason to contain "${expected}"\nActual: ${result.denialReason}`
151
+ ).toBe(true);
152
+ }
153
+
154
+ // ─────────────────────────────────────────────────────────────────────────────
155
+ // Fixture Assertions
156
+ // ─────────────────────────────────────────────────────────────────────────────
157
+
158
+ /**
159
+ * Assert that a file exists in the fixture.
160
+ */
161
+ export function assertFileExists(fixture: TestFixture, relativePath: string, message?: string): void {
162
+ const fullPath = join(fixture.root, relativePath);
163
+ expect(
164
+ existsSync(fullPath),
165
+ message ?? `Expected file to exist: ${relativePath}`
166
+ ).toBe(true);
167
+ }
168
+
169
+ /**
170
+ * Assert that a file does not exist in the fixture.
171
+ */
172
+ export function assertFileNotExists(fixture: TestFixture, relativePath: string, message?: string): void {
173
+ const fullPath = join(fixture.root, relativePath);
174
+ expect(
175
+ existsSync(fullPath),
176
+ message ?? `Expected file to not exist: ${relativePath}`
177
+ ).toBe(false);
178
+ }
179
+
180
+ /**
181
+ * Assert file content contains expected text.
182
+ */
183
+ export function assertFileContains(
184
+ fixture: TestFixture,
185
+ relativePath: string,
186
+ expected: string,
187
+ message?: string
188
+ ): void {
189
+ assertFileExists(fixture, relativePath);
190
+ const content = readFileSync(join(fixture.root, relativePath), 'utf-8');
191
+ expect(
192
+ content.includes(expected),
193
+ message ?? `Expected ${relativePath} to contain "${expected}"\nActual:\n${content.substring(0, 500)}`
194
+ ).toBe(true);
195
+ }
196
+
197
+ /**
198
+ * Assert file content matches regex.
199
+ */
200
+ export function assertFileMatches(
201
+ fixture: TestFixture,
202
+ relativePath: string,
203
+ pattern: RegExp,
204
+ message?: string
205
+ ): void {
206
+ assertFileExists(fixture, relativePath);
207
+ const content = readFileSync(join(fixture.root, relativePath), 'utf-8');
208
+ expect(
209
+ pattern.test(content),
210
+ message ?? `Expected ${relativePath} to match ${pattern}`
211
+ ).toBe(true);
212
+ }
213
+
214
+ /**
215
+ * Assert file has valid YAML frontmatter.
216
+ */
217
+ export function assertValidFrontmatter(
218
+ fixture: TestFixture,
219
+ relativePath: string,
220
+ requiredFields: string[] = []
221
+ ): void {
222
+ assertFileExists(fixture, relativePath);
223
+ const content = readFileSync(join(fixture.root, relativePath), 'utf-8');
224
+
225
+ // Check frontmatter exists
226
+ expect(content.startsWith('---'), `Expected ${relativePath} to start with frontmatter`).toBe(true);
227
+
228
+ const endIndex = content.indexOf('---', 3);
229
+ expect(endIndex > 3, `Expected ${relativePath} to have closing frontmatter delimiter`).toBe(true);
230
+
231
+ const frontmatter = content.substring(3, endIndex).trim();
232
+
233
+ // Check required fields
234
+ for (const field of requiredFields) {
235
+ expect(
236
+ frontmatter.includes(`${field}:`),
237
+ `Expected frontmatter to contain "${field}" field`
238
+ ).toBe(true);
239
+ }
240
+ }
241
+
242
+ // ─────────────────────────────────────────────────────────────────────────────
243
+ // Git Assertions
244
+ // ─────────────────────────────────────────────────────────────────────────────
245
+
246
+ /**
247
+ * Assert file is tracked by git.
248
+ */
249
+ export async function assertGitTracked(
250
+ fixture: TestFixture,
251
+ relativePath: string,
252
+ message?: string
253
+ ): Promise<void> {
254
+ const { execSync } = await import('child_process');
255
+ try {
256
+ execSync(`git ls-files --error-unmatch "${relativePath}"`, {
257
+ cwd: fixture.root,
258
+ stdio: 'pipe',
259
+ });
260
+ } catch {
261
+ expect.fail(message ?? `Expected ${relativePath} to be tracked by git`);
262
+ }
263
+ }
264
+
265
+ /**
266
+ * Assert file has uncommitted changes.
267
+ */
268
+ export async function assertGitDirty(
269
+ fixture: TestFixture,
270
+ relativePath: string,
271
+ message?: string
272
+ ): Promise<void> {
273
+ const { execSync } = await import('child_process');
274
+ const status = execSync(`git status --porcelain "${relativePath}"`, {
275
+ cwd: fixture.root,
276
+ encoding: 'utf-8',
277
+ });
278
+ expect(
279
+ status.trim().length > 0,
280
+ message ?? `Expected ${relativePath} to have uncommitted changes`
281
+ ).toBe(true);
282
+ }
283
+
284
+ // ─────────────────────────────────────────────────────────────────────────────
285
+ // Composite Assertions
286
+ // ─────────────────────────────────────────────────────────────────────────────
287
+
288
+ /**
289
+ * Assert a complete command workflow succeeded.
290
+ */
291
+ export function assertWorkflowSuccess(
292
+ results: { name: string; result: RunResult }[],
293
+ message?: string
294
+ ): void {
295
+ const failures = results.filter((r) => !r.result.success);
296
+ if (failures.length > 0) {
297
+ const details = failures
298
+ .map((f) => ` ${f.name}: exit ${f.result.exitCode}, stderr: ${f.result.stderr}`)
299
+ .join('\n');
300
+ expect.fail(message ?? `Workflow had ${failures.length} failures:\n${details}`);
301
+ }
302
+ }
303
+
304
+ /**
305
+ * Assert all hook contracts passed.
306
+ */
307
+ export function assertContractsPassed(
308
+ results: { contract: { name: string }; passed: boolean; failures: string[] }[],
309
+ message?: string
310
+ ): void {
311
+ const failed = results.filter((r) => !r.passed);
312
+ if (failed.length > 0) {
313
+ const details = failed
314
+ .map((f) => ` ${f.contract.name}:\n ${f.failures.join('\n ')}`)
315
+ .join('\n');
316
+ expect.fail(message ?? `${failed.length} contracts failed:\n${details}`);
317
+ }
318
+ }
@@ -0,0 +1,359 @@
1
+ /**
2
+ * CLI Runner - Execute ah commands and capture output
3
+ *
4
+ * Provides utilities for running the CLI headlessly and asserting on results.
5
+ */
6
+
7
+ import { spawn, SpawnOptions, ChildProcess } from 'child_process';
8
+ import { join } from 'path';
9
+ import type { TestFixture } from './fixture.js';
10
+
11
+ // ─────────────────────────────────────────────────────────────────────────────
12
+ // Types
13
+ // ─────────────────────────────────────────────────────────────────────────────
14
+
15
+ export interface RunOptions {
16
+ /** Working directory for the command */
17
+ cwd?: string;
18
+ /** Environment variables to set */
19
+ env?: Record<string, string>;
20
+ /** Timeout in milliseconds (default: 30000) */
21
+ timeout?: number;
22
+ /** Input to send to stdin */
23
+ stdin?: string;
24
+ /** Whether to expect JSON output */
25
+ expectJson?: boolean;
26
+ }
27
+
28
+ export interface RunResult {
29
+ /** Exit code (0 = success) */
30
+ exitCode: number;
31
+ /** Stdout as string */
32
+ stdout: string;
33
+ /** Stderr as string */
34
+ stderr: string;
35
+ /** Combined stdout + stderr in order received */
36
+ combined: string;
37
+ /** Whether the command succeeded (exit code 0) */
38
+ success: boolean;
39
+ /** Parsed JSON if expectJson was true and output is valid JSON */
40
+ json?: unknown;
41
+ /** Duration in milliseconds */
42
+ duration: number;
43
+ }
44
+
45
+ // ─────────────────────────────────────────────────────────────────────────────
46
+ // CLI Paths
47
+ // ─────────────────────────────────────────────────────────────────────────────
48
+
49
+ /**
50
+ * Get the path to the ah CLI entry point.
51
+ */
52
+ function getCliPath(): string {
53
+ // From src/__tests__/harness/ go up to harness root
54
+ return join(__dirname, '..', '..', 'cli.ts');
55
+ }
56
+
57
+ /**
58
+ * Get the tsx executable path.
59
+ */
60
+ function getTsxPath(): string {
61
+ // Use npx tsx to run TypeScript directly
62
+ return 'npx';
63
+ }
64
+
65
+ // ─────────────────────────────────────────────────────────────────────────────
66
+ // Core Runner
67
+ // ─────────────────────────────────────────────────────────────────────────────
68
+
69
+ /**
70
+ * Run an ah CLI command and capture output.
71
+ */
72
+ export async function runCli(args: string[], options: RunOptions = {}): Promise<RunResult> {
73
+ const {
74
+ cwd = process.cwd(),
75
+ env = {},
76
+ timeout = 30000,
77
+ stdin,
78
+ expectJson = false,
79
+ } = options;
80
+
81
+ const startTime = Date.now();
82
+ const cliPath = getCliPath();
83
+
84
+ return new Promise((resolve) => {
85
+ const spawnEnv: Record<string, string> = {
86
+ ...process.env,
87
+ ...env,
88
+ // Disable color output for consistent parsing
89
+ NO_COLOR: '1',
90
+ FORCE_COLOR: '0',
91
+ } as Record<string, string>;
92
+
93
+ const spawnOptions: SpawnOptions = {
94
+ cwd,
95
+ env: spawnEnv,
96
+ stdio: ['pipe', 'pipe', 'pipe'],
97
+ };
98
+
99
+ // Use tsx to run the CLI TypeScript directly
100
+ const child: ChildProcess = spawn('npx', ['tsx', cliPath, ...args], spawnOptions);
101
+
102
+ let stdout = '';
103
+ let stderr = '';
104
+ let combined = '';
105
+ let timedOut = false;
106
+
107
+ const timeoutId = setTimeout(() => {
108
+ timedOut = true;
109
+ child.kill('SIGTERM');
110
+ }, timeout);
111
+
112
+ child.stdout?.on('data', (data: Buffer) => {
113
+ const text = data.toString();
114
+ stdout += text;
115
+ combined += text;
116
+ });
117
+
118
+ child.stderr?.on('data', (data: Buffer) => {
119
+ const text = data.toString();
120
+ stderr += text;
121
+ combined += text;
122
+ });
123
+
124
+ if (stdin) {
125
+ child.stdin?.write(stdin);
126
+ child.stdin?.end();
127
+ } else {
128
+ child.stdin?.end();
129
+ }
130
+
131
+ child.on('close', (code) => {
132
+ clearTimeout(timeoutId);
133
+ const duration = Date.now() - startTime;
134
+
135
+ const result: RunResult = {
136
+ exitCode: timedOut ? -1 : (code ?? 1),
137
+ stdout: stdout.trim(),
138
+ stderr: stderr.trim(),
139
+ combined: combined.trim(),
140
+ success: !timedOut && code === 0,
141
+ duration,
142
+ };
143
+
144
+ // Try to parse JSON if requested
145
+ if (expectJson && stdout.trim()) {
146
+ try {
147
+ result.json = JSON.parse(stdout.trim());
148
+ } catch {
149
+ // Leave json undefined if parsing fails
150
+ }
151
+ }
152
+
153
+ resolve(result);
154
+ });
155
+
156
+ child.on('error', (err) => {
157
+ clearTimeout(timeoutId);
158
+ const duration = Date.now() - startTime;
159
+
160
+ resolve({
161
+ exitCode: -1,
162
+ stdout: '',
163
+ stderr: err.message,
164
+ combined: err.message,
165
+ success: false,
166
+ duration,
167
+ });
168
+ });
169
+ });
170
+ }
171
+
172
+ /**
173
+ * Run a command in the context of a test fixture.
174
+ */
175
+ export async function runInFixture(
176
+ fixture: TestFixture,
177
+ args: string[],
178
+ options: Omit<RunOptions, 'cwd'> = {}
179
+ ): Promise<RunResult> {
180
+ return runCli(args, {
181
+ ...options,
182
+ cwd: fixture.root,
183
+ env: {
184
+ ...fixture.env,
185
+ ...options.env,
186
+ },
187
+ });
188
+ }
189
+
190
+ // ─────────────────────────────────────────────────────────────────────────────
191
+ // Convenience Wrappers
192
+ // ─────────────────────────────────────────────────────────────────────────────
193
+
194
+ /**
195
+ * Run a knowledge search command.
196
+ */
197
+ export async function runKnowledgeSearch(
198
+ query: string,
199
+ fixture: TestFixture,
200
+ options: { path?: string; k?: number } = {}
201
+ ): Promise<RunResult> {
202
+ const args = ['knowledge', 'search', query];
203
+
204
+ if (options.path) {
205
+ args.push('--path', options.path);
206
+ }
207
+ if (options.k) {
208
+ args.push('--k', options.k.toString());
209
+ }
210
+
211
+ return runInFixture(fixture, args);
212
+ }
213
+
214
+ /**
215
+ * Run a validation command.
216
+ * Uses `ah validate file <path>` syntax.
217
+ */
218
+ export async function runValidate(
219
+ filePath: string,
220
+ fixture: TestFixture
221
+ ): Promise<RunResult> {
222
+ // If path is relative, make it absolute relative to fixture
223
+ const { isAbsolute } = await import('path');
224
+ const absPath = isAbsolute(filePath) ? filePath : join(fixture.root, filePath);
225
+ return runInFixture(fixture, ['validate', 'file', absPath], { expectJson: true });
226
+ }
227
+
228
+ /**
229
+ * Run a spawn codesearch command.
230
+ */
231
+ export async function runCodeSearch(
232
+ query: string,
233
+ fixture: TestFixture,
234
+ options: { budget?: number } = {}
235
+ ): Promise<RunResult> {
236
+ const args = ['spawn', 'codesearch', query];
237
+
238
+ if (options.budget) {
239
+ args.push('--budget', options.budget.toString());
240
+ }
241
+
242
+ return runInFixture(fixture, args, { timeout: 60000 });
243
+ }
244
+
245
+ /**
246
+ * Run a tools list command.
247
+ */
248
+ export async function runToolsList(fixture: TestFixture): Promise<RunResult> {
249
+ return runInFixture(fixture, ['tools', 'list'], { expectJson: true });
250
+ }
251
+
252
+ /**
253
+ * Run a specs list command.
254
+ */
255
+ export async function runSpecsList(fixture: TestFixture): Promise<RunResult> {
256
+ return runInFixture(fixture, ['specs', 'list']);
257
+ }
258
+
259
+ // ─────────────────────────────────────────────────────────────────────────────
260
+ // Batch Runner
261
+ // ─────────────────────────────────────────────────────────────────────────────
262
+
263
+ export interface BatchCommand {
264
+ name: string;
265
+ args: string[];
266
+ options?: RunOptions;
267
+ /** Expected outcome for assertions */
268
+ expect?: {
269
+ success?: boolean;
270
+ exitCode?: number;
271
+ stdoutContains?: string[];
272
+ stderrContains?: string[];
273
+ };
274
+ }
275
+
276
+ export interface BatchResult {
277
+ command: BatchCommand;
278
+ result: RunResult;
279
+ passed: boolean;
280
+ failures: string[];
281
+ }
282
+
283
+ /**
284
+ * Run multiple commands in sequence and collect results.
285
+ */
286
+ export async function runBatch(
287
+ commands: BatchCommand[],
288
+ fixture: TestFixture
289
+ ): Promise<BatchResult[]> {
290
+ const results: BatchResult[] = [];
291
+
292
+ for (const command of commands) {
293
+ const result = await runInFixture(fixture, command.args, command.options);
294
+ const failures: string[] = [];
295
+
296
+ if (command.expect) {
297
+ const { expect: exp } = command;
298
+
299
+ if (exp.success !== undefined && result.success !== exp.success) {
300
+ failures.push(`Expected success=${exp.success}, got ${result.success}`);
301
+ }
302
+
303
+ if (exp.exitCode !== undefined && result.exitCode !== exp.exitCode) {
304
+ failures.push(`Expected exitCode=${exp.exitCode}, got ${result.exitCode}`);
305
+ }
306
+
307
+ if (exp.stdoutContains) {
308
+ for (const expected of exp.stdoutContains) {
309
+ if (!result.stdout.includes(expected)) {
310
+ failures.push(`Expected stdout to contain "${expected}"`);
311
+ }
312
+ }
313
+ }
314
+
315
+ if (exp.stderrContains) {
316
+ for (const expected of exp.stderrContains) {
317
+ if (!result.stderr.includes(expected)) {
318
+ failures.push(`Expected stderr to contain "${expected}"`);
319
+ }
320
+ }
321
+ }
322
+ }
323
+
324
+ results.push({
325
+ command,
326
+ result,
327
+ passed: failures.length === 0,
328
+ failures,
329
+ });
330
+ }
331
+
332
+ return results;
333
+ }
334
+
335
+ // ─────────────────────────────────────────────────────────────────────────────
336
+ // Debug Helpers
337
+ // ─────────────────────────────────────────────────────────────────────────────
338
+
339
+ /**
340
+ * Print a run result for debugging.
341
+ */
342
+ export function debugResult(result: RunResult, label?: string): void {
343
+ console.log('\n' + '='.repeat(60));
344
+ if (label) {
345
+ console.log(`DEBUG: ${label}`);
346
+ console.log('-'.repeat(60));
347
+ }
348
+ console.log(`Exit Code: ${result.exitCode} (${result.success ? 'success' : 'failure'})`);
349
+ console.log(`Duration: ${result.duration}ms`);
350
+ console.log('\n--- STDOUT ---');
351
+ console.log(result.stdout || '(empty)');
352
+ console.log('\n--- STDERR ---');
353
+ console.log(result.stderr || '(empty)');
354
+ if (result.json) {
355
+ console.log('\n--- PARSED JSON ---');
356
+ console.log(JSON.stringify(result.json, null, 2));
357
+ }
358
+ console.log('='.repeat(60) + '\n');
359
+ }