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,926 @@
1
+ /**
2
+ * Oracle - Harness-Specific AI Tasks
3
+ *
4
+ * High-level AI functions specific to the All Hands harness.
5
+ * These are INTERNAL functions - not exposed to agents via CLI.
6
+ *
7
+ * Uses llm.ts for the underlying provider integration.
8
+ *
9
+ * Functions:
10
+ * - generatePRDescription() - Generate PR content from spec + alignment
11
+ * - analyzeConversation() - Analyze agent conversation for compaction
12
+ * - recommendAction() - Recommend continue vs scratch based on analysis
13
+ * - buildPR() - Create PR via gh CLI with generated description
14
+ */
15
+
16
+ import { spawnSync } from 'child_process';
17
+ import { existsSync, readFileSync } from 'fs';
18
+ import { join } from 'path';
19
+ import { z } from 'zod';
20
+ import { ask, getCompactionProvider } from './llm.js';
21
+ import {
22
+ readAlignment,
23
+ readAlignmentFrontmatter,
24
+ readStatus,
25
+ updatePRStatus,
26
+ getPlanningPaths,
27
+ getGitRoot,
28
+ sanitizeBranchForDir,
29
+ getCurrentBranch,
30
+ } from './planning.js';
31
+ import { getBaseBranch, gitExec, validateGitRef, syncWithOriginMain } from './git.js';
32
+ import { logEvent } from './trace-store.js';
33
+
34
+ // ============================================================================
35
+ // Zod Schemas for LLM Response Validation
36
+ // ============================================================================
37
+
38
+ const PRContentSchema = z.object({
39
+ title: z.string(),
40
+ body: z.string(),
41
+ reviewSteps: z.string(),
42
+ });
43
+
44
+ // Use coerce to handle LLMs returning strings instead of proper types
45
+ // e.g., "true" -> true, "65" -> 65
46
+ const ConversationAnalysisSchema = z.object({
47
+ wasGettingClose: z.coerce.boolean(),
48
+ progressPercentage: z.coerce.number().min(0).max(100),
49
+ keyLearnings: z.array(z.string()),
50
+ blockers: z.array(z.string()),
51
+ partialWork: z.array(z.string()),
52
+ });
53
+
54
+ const ActionRecommendationSchema = z.object({
55
+ action: z.enum(['scratch', 'continue']),
56
+ reasoning: z.string(),
57
+ preserveFiles: z.array(z.string()),
58
+ discardFiles: z.array(z.string()),
59
+ });
60
+
61
+ /**
62
+ * Extract JSON from LLM response text.
63
+ * Handles responses wrapped in markdown code blocks or bare JSON.
64
+ */
65
+ function extractJSON(text: string): string | null {
66
+ // Try markdown code block first (```json ... ``` or ``` ... ```)
67
+ const codeBlockMatch = text.match(/```(?:json)?\n?([\s\S]*?)\n?```/);
68
+ if (codeBlockMatch) {
69
+ return codeBlockMatch[1].trim();
70
+ }
71
+
72
+ // Fall back to finding first complete JSON object
73
+ // Find the first { and match to its closing }
74
+ const startIdx = text.indexOf('{');
75
+ if (startIdx === -1) return null;
76
+
77
+ let depth = 0;
78
+ let inString = false;
79
+ let escape = false;
80
+
81
+ for (let i = startIdx; i < text.length; i++) {
82
+ const char = text[i];
83
+
84
+ if (escape) {
85
+ escape = false;
86
+ continue;
87
+ }
88
+
89
+ if (char === '\\' && inString) {
90
+ escape = true;
91
+ continue;
92
+ }
93
+
94
+ if (char === '"' && !escape) {
95
+ inString = !inString;
96
+ continue;
97
+ }
98
+
99
+ if (!inString) {
100
+ if (char === '{') depth++;
101
+ if (char === '}') {
102
+ depth--;
103
+ if (depth === 0) {
104
+ return text.slice(startIdx, i + 1);
105
+ }
106
+ }
107
+ }
108
+ }
109
+
110
+ return null;
111
+ }
112
+
113
+ // ============================================================================
114
+ // Types
115
+ // ============================================================================
116
+
117
+ export interface PRContent {
118
+ title: string;
119
+ body: string;
120
+ reviewSteps: string;
121
+ }
122
+
123
+ export interface ConversationAnalysis {
124
+ wasGettingClose: boolean;
125
+ progressPercentage: number;
126
+ keyLearnings: string[];
127
+ blockers: string[];
128
+ partialWork: string[];
129
+ }
130
+
131
+ export interface ActionRecommendation {
132
+ action: 'scratch' | 'continue';
133
+ reasoning: string;
134
+ preserveFiles: string[];
135
+ discardFiles: string[];
136
+ }
137
+
138
+ export interface BuildPRResult {
139
+ success: boolean;
140
+ prUrl?: string;
141
+ prNumber?: number;
142
+ title: string;
143
+ body: string;
144
+ reviewSteps?: string;
145
+ existingPR?: boolean; // True if PR already existed and was reused
146
+ }
147
+
148
+ // ============================================================================
149
+ // PR Generation (Internal)
150
+ // ============================================================================
151
+
152
+ /**
153
+ * Get git diff from base branch to current branch
154
+ */
155
+ function getGitDiffFromBase(cwd?: string, maxLines: number = 300): string {
156
+ const workingDir = cwd || process.cwd();
157
+
158
+ try {
159
+ // Use the configured base branch
160
+ const baseBranch = getBaseBranch();
161
+ validateGitRef(baseBranch, 'baseBranch');
162
+
163
+ // Get diff stat summary
164
+ const diffStatResult = gitExec(['diff', `${baseBranch}...HEAD`, '--stat'], workingDir);
165
+ const diffStat = diffStatResult.success ? diffStatResult.stdout : '';
166
+
167
+ // Get actual diff (truncated)
168
+ const diffResult = gitExec(['diff', `${baseBranch}...HEAD`], workingDir);
169
+ const diff = diffResult.stdout;
170
+
171
+ const lines = diff.split('\n');
172
+ const truncatedDiff = lines.length > maxLines
173
+ ? lines.slice(0, maxLines).join('\n') + '\n... (truncated)'
174
+ : diff;
175
+
176
+ return `### Summary\n${diffStat}\n\n### Changes\n${truncatedDiff}`;
177
+ } catch {
178
+ return 'Unable to get git diff';
179
+ }
180
+ }
181
+
182
+ /**
183
+ * Generate a PR description from prompts and alignment doc
184
+ *
185
+ * INTERNAL ONLY - Not exposed via CLI to agents.
186
+ * Used by TUI for create-pr functionality.
187
+ * Uses Gemini provider for generation.
188
+ */
189
+ export async function generatePRDescription(
190
+ alignmentContent: string,
191
+ specName: string,
192
+ cwd?: string,
193
+ specContent?: string
194
+ ): Promise<PRContent> {
195
+ const gitDiff = getGitDiffFromBase(cwd);
196
+
197
+ // Parse changed files from git diff for review steps grouping
198
+ const changedFiles = parseChangedFilesFromDiff(gitDiff);
199
+
200
+ const prompt = `Generate a pull request title and description.
201
+
202
+ ## Original Requirements (Spec File):
203
+ ${specContent || 'Not provided'}
204
+
205
+ ## Implementation Summary (Alignment Document):
206
+ ${alignmentContent}
207
+
208
+ ## Changed Files:
209
+ ${changedFiles.join('\n')}
210
+
211
+ ## Instructions:
212
+ Write a standard, concise PR description like you would see on any professional open source project.
213
+ - PR title: Clear, under 72 chars, describes what was implemented
214
+ - PR body: Brief summary of what was built and why, followed by a test plan
215
+ - Do NOT reference individual prompts, tasks, or implementation steps
216
+ - Focus on the end result and value delivered
217
+ - Keep it concise - this is a PR description, not documentation
218
+
219
+ ## Review Steps Requirements:
220
+ Group the changed files into logical review buckets based on the alignment document and file relationships.
221
+ - Group related files together (e.g., API + its tests, component + styles)
222
+ - Order by review priority: Core logic → API/interfaces → Utilities → Tests → Config/docs
223
+ - For each bucket, briefly note what to look for
224
+ - Keep it scannable - bullet points, not paragraphs
225
+
226
+ ## Response Format (JSON only):
227
+ {
228
+ "title": "Short PR title",
229
+ "body": "## Summary\\n\\nBrief description.\\n\\n## Test Plan\\n\\n- How to test",
230
+ "reviewSteps": "## Review Steps\\n\\n### 1. Core Logic\\n- file1.ts\\n- file2.ts\\n\\nLook for: X, Y\\n\\n### 2. Tests\\n..."
231
+ }`;
232
+
233
+ try {
234
+ const result = await ask(prompt, {
235
+ provider: 'gemini',
236
+ context: 'You must respond with valid JSON only. No markdown code blocks.',
237
+ });
238
+
239
+ const jsonStr = extractJSON(result.text);
240
+ if (!jsonStr) {
241
+ throw new Error('No JSON found in response');
242
+ }
243
+
244
+ const parsed = PRContentSchema.parse(JSON.parse(jsonStr));
245
+ return parsed;
246
+ } catch {
247
+ // Fallback: Extract a summary from alignment doc instead of listing prompts
248
+ const alignmentSummary = extractAlignmentSummary(alignmentContent);
249
+ return {
250
+ title: `${specName}`,
251
+ body: `## Summary\n\n${alignmentSummary}\n\n## Test Plan\n\n- Run the test suite\n- Manual verification of core functionality`,
252
+ reviewSteps: generateFallbackReviewSteps(changedFiles),
253
+ };
254
+ }
255
+ }
256
+
257
+ /**
258
+ * Parse changed file paths from git diff output
259
+ */
260
+ function parseChangedFilesFromDiff(gitDiff: string): string[] {
261
+ const files: string[] = [];
262
+ const lines = gitDiff.split('\n');
263
+ for (const line of lines) {
264
+ // Match "diff --git a/path b/path" format
265
+ const match = line.match(/^diff --git a\/(.+) b\//);
266
+ if (match) {
267
+ files.push(match[1]);
268
+ }
269
+ }
270
+ return files;
271
+ }
272
+
273
+ /**
274
+ * Extract a brief summary from the alignment document
275
+ */
276
+ function extractAlignmentSummary(alignmentContent: string): string {
277
+ if (!alignmentContent) {
278
+ return 'Implementation complete.';
279
+ }
280
+
281
+ // Try to find an overview or summary section
282
+ const overviewMatch = alignmentContent.match(/## Overview\s*\n([\s\S]*?)(?=\n##|$)/i);
283
+ if (overviewMatch) {
284
+ const overview = overviewMatch[1].trim();
285
+ // Take first paragraph or first 500 chars
286
+ const firstParagraph = overview.split('\n\n')[0];
287
+ return firstParagraph.slice(0, 500);
288
+ }
289
+
290
+ // Fallback: take content after frontmatter, first paragraph
291
+ const withoutFrontmatter = alignmentContent.replace(/^---[\s\S]*?---\n/, '');
292
+ const firstParagraph = withoutFrontmatter.trim().split('\n\n')[0];
293
+ return firstParagraph.slice(0, 500) || 'Implementation complete.';
294
+ }
295
+
296
+ /**
297
+ * Generate fallback review steps grouped by file type
298
+ */
299
+ function generateFallbackReviewSteps(files: string[]): string {
300
+ const groups: Record<string, string[]> = {
301
+ 'Core Logic': [],
302
+ 'API/Routes': [],
303
+ 'Components': [],
304
+ 'Tests': [],
305
+ 'Configuration': [],
306
+ 'Other': [],
307
+ };
308
+
309
+ for (const file of files) {
310
+ if (file.includes('.test.') || file.includes('.spec.') || file.includes('__tests__')) {
311
+ groups['Tests'].push(file);
312
+ } else if (file.includes('/api/') || file.includes('/routes/') || file.includes('router')) {
313
+ groups['API/Routes'].push(file);
314
+ } else if (file.includes('/components/') || file.includes('.tsx')) {
315
+ groups['Components'].push(file);
316
+ } else if (file.match(/\.(json|yaml|yml|config\.|rc\.)/) || file.includes('config')) {
317
+ groups['Configuration'].push(file);
318
+ } else if (file.match(/\.(ts|js|py|go|rs)$/)) {
319
+ groups['Core Logic'].push(file);
320
+ } else {
321
+ groups['Other'].push(file);
322
+ }
323
+ }
324
+
325
+ let steps = '## Review Steps\n\n';
326
+ let stepNum = 1;
327
+
328
+ for (const [groupName, groupFiles] of Object.entries(groups)) {
329
+ if (groupFiles.length > 0) {
330
+ steps += `### ${stepNum}. ${groupName}\n`;
331
+ for (const f of groupFiles) {
332
+ steps += `- ${f}\n`;
333
+ }
334
+ steps += '\n';
335
+ stepNum++;
336
+ }
337
+ }
338
+
339
+ return steps || '## Review Steps\n\nReview all changed files.';
340
+ }
341
+
342
+ // ============================================================================
343
+ // Conversation Analysis (Internal)
344
+ // ============================================================================
345
+
346
+ /**
347
+ * Analyze an agent conversation for compaction
348
+ *
349
+ * INTERNAL ONLY - Called by compaction handler after agent session.
350
+ * Examines conversation logs to understand progress and extract learnings.
351
+ */
352
+ export async function analyzeConversation(
353
+ logs: string,
354
+ promptContent: string,
355
+ alignmentContent: string,
356
+ gitDiff: string
357
+ ): Promise<ConversationAnalysis> {
358
+ const prompt = `Analyze this agent conversation and extract useful information for the next attempt.
359
+
360
+ ## Task Prompt:
361
+ ${promptContent}
362
+
363
+ ## Alignment Document:
364
+ ${alignmentContent}
365
+
366
+ ## Git Diff Summary:
367
+ ${gitDiff}
368
+
369
+ ## Conversation Logs:
370
+ ${logs}
371
+
372
+ ## Analysis Required:
373
+
374
+ 1. Was the agent making meaningful progress toward the goal? (yes/no)
375
+ 2. Estimate the percentage of the task that was completed (0-100)
376
+ 3. What key learnings would help a fresh agent? (specific patterns, APIs, approaches that work)
377
+ 4. What blocked the agent from completion? (missing deps, wrong approach, unclear requirements)
378
+ 5. What partial work is valuable and should be preserved? (list specific files or code)
379
+
380
+ ## Response Format (JSON only):
381
+ {
382
+ "wasGettingClose": true,
383
+ "progressPercentage": 65,
384
+ "keyLearnings": ["Pattern X works well for...", "The API requires..."],
385
+ "blockers": ["Missing dependency Z", "Unclear requirement about..."],
386
+ "partialWork": ["src/lib/foo.ts", "src/commands/bar.ts"]
387
+ }`;
388
+
389
+ try {
390
+ const result = await ask(prompt, {
391
+ context: 'You must respond with valid JSON only. No markdown code blocks. Be concise.',
392
+ provider: getCompactionProvider(),
393
+ });
394
+
395
+ const jsonStr = extractJSON(result.text);
396
+ if (!jsonStr) {
397
+ throw new Error('No JSON found in response');
398
+ }
399
+
400
+ return ConversationAnalysisSchema.parse(JSON.parse(jsonStr));
401
+ } catch (error) {
402
+ // Log the actual error for debugging
403
+ const errorMsg = error instanceof Error ? error.message : String(error);
404
+ logEvent('harness.error', {
405
+ source: 'oracle.analyzeConversation',
406
+ error: errorMsg,
407
+ stack: error instanceof Error ? error.stack : undefined,
408
+ });
409
+
410
+ // Conservative fallback - assume some progress was made
411
+ return {
412
+ wasGettingClose: true,
413
+ progressPercentage: 50,
414
+ keyLearnings: [],
415
+ blockers: [`Analysis failed: ${errorMsg}`],
416
+ partialWork: [],
417
+ };
418
+ }
419
+ }
420
+
421
+ /**
422
+ * Recommend whether to continue or scratch based on analysis
423
+ *
424
+ * INTERNAL ONLY - Called by compaction handler.
425
+ * Uses analysis and git diff to decide if code should be kept.
426
+ */
427
+ export async function recommendAction(
428
+ analysis: ConversationAnalysis,
429
+ attemptNumber: number,
430
+ gitDiff: string
431
+ ): Promise<ActionRecommendation> {
432
+ const prompt = `Based on this analysis, recommend whether to continue with the existing code or scratch and start fresh.
433
+
434
+ ## Analysis:
435
+ - Progress: ${analysis.progressPercentage}%
436
+ - Was getting close: ${analysis.wasGettingClose}
437
+ - Key learnings: ${analysis.keyLearnings.join('; ')}
438
+ - Blockers: ${analysis.blockers.join('; ')}
439
+ - Partial work: ${analysis.partialWork.join(', ')}
440
+ - Attempt number: ${attemptNumber}
441
+
442
+ ## Git Diff:
443
+ ${gitDiff}
444
+
445
+ ## Considerations:
446
+ 1. Code stability - Does it compile/run? Is there test coverage?
447
+ 2. Boilerplate vs logic - Is this mostly setup code or actual implementation?
448
+ 3. Complexity of remaining work - How much is left to do?
449
+ 4. Risk of starting fresh - Would we lose valuable progress?
450
+
451
+ ## Decision Guidelines:
452
+ - CONTINUE if: >40% progress, code compiles, meaningful logic exists
453
+ - SCRATCH if: <20% progress, code broken, mostly boilerplate, wrong approach
454
+
455
+ ## Response Format (JSON only):
456
+ {
457
+ "action": "continue",
458
+ "reasoning": "Brief explanation of the decision",
459
+ "preserveFiles": ["files to keep if scratching"],
460
+ "discardFiles": ["files that should be removed"]
461
+ }`;
462
+
463
+ try {
464
+ const result = await ask(prompt, {
465
+ context: 'You must respond with valid JSON only. No markdown code blocks.',
466
+ provider: getCompactionProvider(),
467
+ });
468
+
469
+ const jsonStr = extractJSON(result.text);
470
+ if (!jsonStr) {
471
+ throw new Error('No JSON found in response');
472
+ }
473
+
474
+ // Zod validates action is 'scratch' | 'continue'
475
+ return ActionRecommendationSchema.parse(JSON.parse(jsonStr));
476
+ } catch (error) {
477
+ // Log the actual error for debugging
478
+ const errorMsg = error instanceof Error ? error.message : String(error);
479
+ logEvent('harness.error', {
480
+ source: 'oracle.recommendAction',
481
+ error: errorMsg,
482
+ stack: error instanceof Error ? error.stack : undefined,
483
+ });
484
+
485
+ // Default to continue to avoid losing code
486
+ return {
487
+ action: 'continue',
488
+ reasoning: `Defaulting to continue: ${errorMsg}`,
489
+ preserveFiles: [],
490
+ discardFiles: [],
491
+ };
492
+ }
493
+ }
494
+
495
+ // ============================================================================
496
+ // PR Building (Internal)
497
+ // ============================================================================
498
+
499
+ /**
500
+ * Build and create a PR with generated description
501
+ *
502
+ * INTERNAL ONLY - Called by pr-build command.
503
+ * Uses generatePRDescription and gh CLI to create PR.
504
+ *
505
+ * @param spec - The spec name to build PR for
506
+ * @param cwd - Working directory
507
+ * @param dryRun - If true, don't actually create the PR
508
+ */
509
+ export async function buildPR(
510
+ spec: string,
511
+ cwd?: string,
512
+ dryRun: boolean = false
513
+ ): Promise<BuildPRResult> {
514
+ const workingDir = cwd || process.cwd();
515
+
516
+ // Check if PR already exists in status.yaml
517
+ const branch = getCurrentBranch(workingDir);
518
+ const planningKey = sanitizeBranchForDir(branch);
519
+ const status = readStatus(planningKey, workingDir);
520
+ const existingPR = status?.pr;
521
+
522
+ // Load alignment and spec content
523
+ const alignmentContent = readAlignment(spec, workingDir);
524
+
525
+ if (!alignmentContent) {
526
+ return {
527
+ success: false,
528
+ title: '',
529
+ body: 'No alignment document found for this spec',
530
+ };
531
+ }
532
+
533
+ // Load spec content from the spec file path in alignment frontmatter
534
+ const alignmentFrontmatter = readAlignmentFrontmatter(spec, workingDir);
535
+ let specContent: string | undefined;
536
+ if (alignmentFrontmatter?.spec) {
537
+ try {
538
+ const specPath = join(getGitRoot(workingDir), alignmentFrontmatter.spec);
539
+ specContent = readFileSync(specPath, 'utf-8');
540
+ } catch {
541
+ // Non-fatal: spec file might not exist
542
+ }
543
+ }
544
+
545
+ // Generate PR content
546
+ const prContent = await generatePRDescription(
547
+ alignmentContent,
548
+ spec,
549
+ workingDir,
550
+ specContent
551
+ );
552
+
553
+ if (dryRun) {
554
+ return {
555
+ success: true,
556
+ title: prContent.title,
557
+ body: prContent.body,
558
+ reviewSteps: prContent.reviewSteps,
559
+ };
560
+ }
561
+
562
+ // If PR already exists, UPDATE instead of creating
563
+ if (existingPR?.url && existingPR?.number) {
564
+ try {
565
+ // Sync with origin/main before push
566
+ const syncResult = syncWithOriginMain(workingDir);
567
+ if (!syncResult.success) {
568
+ const failureReason = syncResult.conflicts.length > 0
569
+ ? `Merge conflicts with main must be resolved before updating PR:\n${syncResult.conflicts.join('\n')}`
570
+ : 'Failed to sync with main. This can be caused by uncommitted changes or network issues. Please resolve and try again.';
571
+ return {
572
+ success: false,
573
+ title: prContent.title,
574
+ body: failureReason,
575
+ };
576
+ }
577
+
578
+ // Push any new changes first
579
+ gitExec(['push', '-u', 'origin', 'HEAD'], workingDir);
580
+
581
+ // Update PR description
582
+ updatePRDescription(existingPR.number, prContent.body, workingDir);
583
+
584
+ // Update comments (review steps and E2E test plan)
585
+ updatePRComments(existingPR.number, prContent.reviewSteps, spec, workingDir);
586
+
587
+ return {
588
+ success: true,
589
+ prUrl: existingPR.url,
590
+ prNumber: existingPR.number,
591
+ title: prContent.title,
592
+ body: prContent.body,
593
+ reviewSteps: prContent.reviewSteps,
594
+ existingPR: true,
595
+ };
596
+ } catch (error) {
597
+ const errorMsg = error instanceof Error ? error.message : String(error);
598
+ return {
599
+ success: false,
600
+ title: prContent.title,
601
+ body: `PR update failed: ${errorMsg}`,
602
+ };
603
+ }
604
+ }
605
+
606
+ // No existing PR - create new one
607
+ try {
608
+ // Sync with origin/main before push
609
+ const syncResult = syncWithOriginMain(workingDir);
610
+ if (!syncResult.success) {
611
+ const failureReason = syncResult.conflicts.length > 0
612
+ ? `Merge conflicts with main must be resolved before creating PR:\n${syncResult.conflicts.join('\n')}`
613
+ : 'Failed to sync with main. This can be caused by uncommitted changes or network issues. Please resolve and try again.';
614
+ return {
615
+ success: false,
616
+ title: prContent.title,
617
+ body: failureReason,
618
+ };
619
+ }
620
+
621
+ // Push branch to remote before creating PR
622
+ const pushResult = gitExec(['push', '-u', 'origin', 'HEAD'], workingDir);
623
+ if (!pushResult.success && !pushResult.stderr.includes('Everything up-to-date')) {
624
+ return {
625
+ success: false,
626
+ title: prContent.title,
627
+ body: `Failed to push branch: ${pushResult.stderr}`,
628
+ };
629
+ }
630
+
631
+ // Create PR via gh CLI with argument arrays — no shell interpolation
632
+ const prCreateResult = spawnSync('gh', [
633
+ 'pr', 'create',
634
+ '--title', prContent.title,
635
+ '--body-file', '-',
636
+ ], {
637
+ encoding: 'utf-8',
638
+ cwd: workingDir,
639
+ input: prContent.body,
640
+ });
641
+
642
+ if (prCreateResult.status !== 0) {
643
+ const errOutput = prCreateResult.stderr || '';
644
+ // Check if PR already exists (race condition) - update instead
645
+ if (errOutput.includes('already exists') || errOutput.includes('pull request already')) {
646
+ return handleExistingPRRace(prContent, spec, workingDir);
647
+ }
648
+ return {
649
+ success: false,
650
+ title: prContent.title,
651
+ body: `PR creation failed: ${errOutput}`,
652
+ };
653
+ }
654
+
655
+ const output = prCreateResult.stdout || '';
656
+
657
+ // Parse PR URL from output
658
+ const urlMatch = output.match(/https:\/\/github\.com\/[^\s]+\/pull\/(\d+)/);
659
+ const prUrl = urlMatch ? urlMatch[0] : undefined;
660
+ const prNumber = urlMatch ? parseInt(urlMatch[1], 10) : undefined;
661
+
662
+ // Update status.yaml with PR info and post comments
663
+ if (prUrl && prNumber) {
664
+ updatePRStatus(prUrl, prNumber, spec, workingDir);
665
+ postPRComments(prNumber, prContent.reviewSteps, spec, workingDir);
666
+ }
667
+
668
+ return {
669
+ success: true,
670
+ prUrl,
671
+ prNumber,
672
+ title: prContent.title,
673
+ body: prContent.body,
674
+ reviewSteps: prContent.reviewSteps,
675
+ };
676
+ } catch (error) {
677
+ const errorMsg = error instanceof Error ? error.message : String(error);
678
+ return {
679
+ success: false,
680
+ title: prContent.title,
681
+ body: `PR creation failed: ${errorMsg}`,
682
+ };
683
+ }
684
+ }
685
+
686
+ /**
687
+ * Handle race condition where PR already exists during creation.
688
+ * Fetches existing PR info and updates it instead.
689
+ */
690
+ function handleExistingPRRace(
691
+ prContent: PRContent,
692
+ spec: string,
693
+ workingDir: string
694
+ ): BuildPRResult {
695
+ try {
696
+ const prViewResult = spawnSync('gh', ['pr', 'view', '--json', 'url,number'], {
697
+ encoding: 'utf-8',
698
+ cwd: workingDir,
699
+ });
700
+ if (prViewResult.status !== 0) {
701
+ return { success: false, title: prContent.title, body: 'PR already exists but could not retrieve info' };
702
+ }
703
+ const prInfo = JSON.parse(prViewResult.stdout || '{}');
704
+ const raceExistingUrl = prInfo.url;
705
+ const raceExistingNumber = prInfo.number;
706
+
707
+ if (raceExistingUrl && raceExistingNumber) {
708
+ updatePRStatus(raceExistingUrl, raceExistingNumber, spec, workingDir);
709
+ updatePRDescription(raceExistingNumber, prContent.body, workingDir);
710
+ updatePRComments(raceExistingNumber, prContent.reviewSteps, spec, workingDir);
711
+
712
+ return {
713
+ success: true,
714
+ prUrl: raceExistingUrl,
715
+ prNumber: raceExistingNumber,
716
+ title: prContent.title,
717
+ body: prContent.body,
718
+ reviewSteps: prContent.reviewSteps,
719
+ existingPR: true,
720
+ };
721
+ }
722
+ } catch {
723
+ // Couldn't get existing PR info
724
+ }
725
+ return { success: false, title: prContent.title, body: 'PR already exists but could not retrieve info' };
726
+ }
727
+
728
+ /**
729
+ * Post review steps and E2E test plan comments to a PR
730
+ * Order: Review steps first, then E2E test plan
731
+ */
732
+ function postPRComments(
733
+ prNumber: number,
734
+ reviewSteps: string | undefined,
735
+ spec: string,
736
+ workingDir: string
737
+ ): void {
738
+ // Post review steps FIRST
739
+ if (reviewSteps) {
740
+ const result = spawnSync('gh', ['pr', 'comment', String(prNumber), '--body-file', '-'], {
741
+ encoding: 'utf-8',
742
+ cwd: workingDir,
743
+ input: reviewSteps,
744
+ });
745
+ if (result.status !== 0) {
746
+ console.error('Warning: Could not add review steps comment to PR');
747
+ }
748
+ }
749
+
750
+ // Post E2E test plan AFTER review steps
751
+ const planningPaths = getPlanningPaths(spec, workingDir);
752
+ const e2eTestPlanPath = join(planningPaths.root, 'e2e-test-plan.md');
753
+ if (existsSync(e2eTestPlanPath)) {
754
+ let e2eTestPlan = readFileSync(e2eTestPlanPath, 'utf-8');
755
+ // Strip YAML frontmatter if present
756
+ e2eTestPlan = e2eTestPlan.replace(/^---\n[\s\S]*?\n---\n*/, '').trim();
757
+ const result = spawnSync('gh', ['pr', 'comment', String(prNumber), '--body-file', '-'], {
758
+ encoding: 'utf-8',
759
+ cwd: workingDir,
760
+ input: `## E2E Test Plan\n\n${e2eTestPlan}`,
761
+ });
762
+ if (result.status !== 0) {
763
+ console.error('Warning: Could not add e2e test plan comment to PR');
764
+ }
765
+ }
766
+ }
767
+
768
+ /**
769
+ * Update PR description via gh CLI
770
+ */
771
+ function updatePRDescription(
772
+ prNumber: number,
773
+ body: string,
774
+ workingDir: string
775
+ ): boolean {
776
+ const result = spawnSync('gh', ['pr', 'edit', String(prNumber), '--body-file', '-'], {
777
+ encoding: 'utf-8',
778
+ cwd: workingDir,
779
+ input: body,
780
+ });
781
+ if (result.status !== 0) {
782
+ console.error('Warning: Could not update PR description');
783
+ return false;
784
+ }
785
+ return true;
786
+ }
787
+
788
+ interface PRComment {
789
+ id: number;
790
+ body: string;
791
+ author: { login: string };
792
+ createdAt: string;
793
+ }
794
+
795
+ /**
796
+ * Get all issue comments on a PR (these are the top-level comments, not review comments)
797
+ */
798
+ function getPRComments(prNumber: number, workingDir: string): PRComment[] {
799
+ try {
800
+ // Get repo owner/name
801
+ const repoResult = spawnSync('gh', ['repo', 'view', '--json', 'owner,name'], {
802
+ encoding: 'utf-8',
803
+ cwd: workingDir,
804
+ });
805
+ if (repoResult.status !== 0) return [];
806
+ const { owner, name } = JSON.parse(repoResult.stdout || '{}');
807
+
808
+ // Get issue comments (top-level PR comments)
809
+ const commentsResult = spawnSync('gh', [
810
+ 'api', `repos/${owner.login}/${name}/issues/${prNumber}/comments`,
811
+ '--jq', '[.[] | {id: .id, body: .body, author: {login: .user.login}, createdAt: .created_at}]',
812
+ ], {
813
+ encoding: 'utf-8',
814
+ cwd: workingDir,
815
+ });
816
+ if (commentsResult.status !== 0) return [];
817
+ return JSON.parse(commentsResult.stdout || '[]');
818
+ } catch {
819
+ return [];
820
+ }
821
+ }
822
+
823
+ /**
824
+ * Update a specific comment on a PR
825
+ */
826
+ function updatePRComment(
827
+ commentId: number,
828
+ body: string,
829
+ workingDir: string
830
+ ): boolean {
831
+ try {
832
+ // Get repo owner/name
833
+ const repoResult = spawnSync('gh', ['repo', 'view', '--json', 'owner,name'], {
834
+ encoding: 'utf-8',
835
+ cwd: workingDir,
836
+ });
837
+ if (repoResult.status !== 0) return false;
838
+ const { owner, name } = JSON.parse(repoResult.stdout || '{}');
839
+
840
+ const patchResult = spawnSync('gh', [
841
+ 'api', '--method', 'PATCH',
842
+ `repos/${owner.login}/${name}/issues/comments/${commentId}`,
843
+ '-f', 'body=@-',
844
+ ], {
845
+ encoding: 'utf-8',
846
+ cwd: workingDir,
847
+ input: body,
848
+ });
849
+ return patchResult.status === 0;
850
+ } catch {
851
+ return false;
852
+ }
853
+ }
854
+
855
+ /**
856
+ * Update existing PR comments (review steps and E2E test plan)
857
+ * Finds comments by pattern matching and updates them
858
+ */
859
+ function updatePRComments(
860
+ prNumber: number,
861
+ reviewSteps: string | undefined,
862
+ spec: string,
863
+ workingDir: string
864
+ ): void {
865
+ const comments = getPRComments(prNumber, workingDir);
866
+ if (comments.length === 0) {
867
+ // No existing comments, post new ones
868
+ postPRComments(prNumber, reviewSteps, spec, workingDir);
869
+ return;
870
+ }
871
+
872
+ // Find and update review steps comment (first comment or one with ## File Walkthrough)
873
+ if (reviewSteps) {
874
+ // Look for comment containing file walkthrough patterns
875
+ const reviewComment = comments.find(c =>
876
+ c.body.includes('## File Walkthrough') ||
877
+ c.body.includes('## Review Steps') ||
878
+ c.body.includes('## Changes Overview')
879
+ );
880
+
881
+ if (reviewComment) {
882
+ updatePRComment(reviewComment.id, reviewSteps, workingDir);
883
+ } else {
884
+ // No matching comment found, post new one
885
+ const result = spawnSync('gh', ['pr', 'comment', String(prNumber), '--body-file', '-'], {
886
+ encoding: 'utf-8',
887
+ cwd: workingDir,
888
+ input: reviewSteps,
889
+ });
890
+ if (result.status !== 0) {
891
+ console.error('Warning: Could not add review steps comment to PR');
892
+ }
893
+ }
894
+ }
895
+
896
+ // Find and update E2E test plan comment
897
+ const planningPaths = getPlanningPaths(spec, workingDir);
898
+ const e2eTestPlanPath = join(planningPaths.root, 'e2e-test-plan.md');
899
+ if (existsSync(e2eTestPlanPath)) {
900
+ let e2eTestPlan = readFileSync(e2eTestPlanPath, 'utf-8');
901
+ // Strip YAML frontmatter if present
902
+ e2eTestPlan = e2eTestPlan.replace(/^---\n[\s\S]*?\n---\n*/, '').trim();
903
+ const e2eBody = `## E2E Test Plan\n\n${e2eTestPlan}`;
904
+
905
+ // Look for existing E2E comment
906
+ const e2eComment = comments.find(c => c.body.includes('## E2E Test Plan'));
907
+
908
+ if (e2eComment) {
909
+ // Check if content is different (compare without the header)
910
+ const existingContent = e2eComment.body.replace(/^## E2E Test Plan\s*\n*/, '').trim();
911
+ if (existingContent !== e2eTestPlan) {
912
+ updatePRComment(e2eComment.id, e2eBody, workingDir);
913
+ }
914
+ } else {
915
+ // No E2E comment exists, post new one
916
+ const result = spawnSync('gh', ['pr', 'comment', String(prNumber), '--body-file', '-'], {
917
+ encoding: 'utf-8',
918
+ cwd: workingDir,
919
+ input: e2eBody,
920
+ });
921
+ if (result.status !== 0) {
922
+ console.error('Warning: Could not add e2e test plan comment to PR');
923
+ }
924
+ }
925
+ }
926
+ }