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,960 @@
1
+ /**
2
+ * TUI Command - Launch the terminal user interface
3
+ *
4
+ * The TUI provides a dashboard for:
5
+ * - Starting/stopping the execution loop
6
+ * - Spawning agent sessions (ideate, coordinator, planner, etc.)
7
+ * - Monitoring loop progress and agent activity
8
+ * - Managing specs and PR workflows
9
+ *
10
+ * In the branch-keyed model:
11
+ * - Current git branch determines the active spec
12
+ * - Switching specs means checking out the spec's branch
13
+ * - Planning directories are keyed by sanitized branch name
14
+ */
15
+
16
+ import { Command } from 'commander';
17
+ import { join, basename, dirname, relative } from 'path';
18
+ import { existsSync, readFileSync, renameSync } from 'fs';
19
+
20
+ import { TUI } from '../tui/index.js';
21
+ import type { TUIState, PRActionState, PromptItem, AgentInfo } from '../tui/index.js';
22
+ import {
23
+ readStatus,
24
+ getCurrentBranch,
25
+ updateStatus,
26
+ updatePRReviewStatus,
27
+ initializeStatus,
28
+ getPlanningPaths,
29
+ ensurePlanningDir,
30
+ sanitizeBranchForDir,
31
+ planningDirExists,
32
+ } from '../lib/planning.js';
33
+ import { triggerPRReview } from '../lib/pr-review.js';
34
+ import { loadProjectSettings } from '../hooks/shared.js';
35
+ import { findSpecForPath, extractSpecNameFromFile } from '../lib/planning-utils.js';
36
+ import { getBaseBranch, hasUncommittedChanges, gitExec, validateGitRef, syncWithOriginMain } from '../lib/git.js';
37
+ import { loadAllPrompts, getNextPromptNumber } from '../lib/prompts.js';
38
+ import {
39
+ isTmuxInstalled,
40
+ spawnAgentFromProfile,
41
+ buildTemplateContext,
42
+ getRunningAgents,
43
+ renameWindow,
44
+ getCurrentWindowId,
45
+ } from '../lib/tmux.js';
46
+ import { setHubWindowId, clearTuiSession } from '../lib/session.js';
47
+ import { getProfilesByTuiAction } from '../lib/opencode/index.js';
48
+ import { buildPR } from '../lib/oracle.js';
49
+ import type { PromptFile } from '../lib/prompts.js';
50
+ import { findSpecById, getSpecForBranch, type SpecFile, type SpecType } from '../lib/specs.js';
51
+ import { updateSpecStatus, reindexAfterMove } from './specs.js';
52
+ import { logTuiError, logTuiAction, logTuiLifecycle } from '../lib/trace-store.js';
53
+ import { getFlowsDirectory } from '../lib/flows.js';
54
+
55
+ /**
56
+ * Unified scoping flow — all spec types route here.
57
+ * Domain-specific behavior is driven by the WORKFLOW_DOMAIN_PATH template variable,
58
+ * not by flow file selection.
59
+ */
60
+ export const UNIFIED_SCOPING_FLOW = 'IDEATION_SCOPING.md';
61
+
62
+ /**
63
+ * Launch the TUI - can be called directly or via command
64
+ */
65
+ export async function launchTUI(options: { spec?: string } = {}): Promise<void> {
66
+ const cwd = process.cwd();
67
+
68
+ // IMMEDIATELY capture window ID before any slow operations (like tldr indexing)
69
+ // This ensures we track the correct window even if user switches away during init
70
+ const hubWindowId = getCurrentWindowId();
71
+ if (hubWindowId) {
72
+ setHubWindowId(hubWindowId, cwd);
73
+ }
74
+
75
+ const branch = getCurrentBranch(cwd);
76
+ const planningKey = sanitizeBranchForDir(branch);
77
+
78
+ // Find current spec from branch using planning dir as source of truth
79
+ const currentSpec = getSpecForBranch(branch, cwd);
80
+
81
+ // TLDR semantic indexing now runs in background after TUI launches (see TUI.startBackgroundIndexing)
82
+
83
+ // Load initial state from current branch's planning directory
84
+ // Load prompts if planning directory exists, regardless of status file
85
+ const status = planningDirExists(planningKey, cwd) ? readStatus(planningKey, cwd) : null;
86
+ const prompts = planningDirExists(planningKey, cwd) ? loadAllPrompts(planningKey, cwd) : [];
87
+
88
+ // Convert prompts to PromptItem format
89
+ const promptItems: PromptItem[] = prompts.map((p) => ({
90
+ number: p.frontmatter.number,
91
+ title: p.frontmatter.title,
92
+ status: p.frontmatter.status as 'pending' | 'in_progress' | 'done',
93
+ path: p.path,
94
+ }));
95
+
96
+ // Get active agents from tmux
97
+ const runningAgents = getRunningAgents(branch);
98
+ const activeAgents: AgentInfo[] = runningAgents.map((a) => ({
99
+ name: a.windowName,
100
+ agentType: a.agentType || 'unknown',
101
+ isRunning: true,
102
+ }));
103
+
104
+ const baseBranch = getBaseBranch();
105
+
106
+ // Determine initial PR action state based on PR and review status
107
+ let initialPRActionState: PRActionState = 'create-pr';
108
+ if (status?.pr?.url) {
109
+ // PR exists - check if review feedback was received
110
+ if (status?.prReview?.lastReviewTime) {
111
+ // Review feedback was received - show rerun option
112
+ initialPRActionState = 'rerun-pr-review';
113
+ } else {
114
+ // PR created but waiting for review
115
+ initialPRActionState = 'awaiting-review';
116
+ }
117
+ }
118
+
119
+ const initialState: Partial<TUIState> = {
120
+ loopEnabled: false, // Always start disabled, regardless of saved status
121
+ parallelEnabled: status?.loop?.parallel ?? false,
122
+ prompts: promptItems,
123
+ activeAgents,
124
+ spec: currentSpec?.id,
125
+ branch,
126
+ baseBranch,
127
+ prActionState: initialPRActionState,
128
+ customFlowCounter: 0,
129
+ };
130
+
131
+ // Rename the hub window (using captured ID to target correct window even if focus changed)
132
+ renameWindow(hubWindowId, 'hub');
133
+
134
+ const tui = new TUI({
135
+ onAction: (action: string, data) => {
136
+ // Get current branch/spec from TUI state (not captured at init time)
137
+ const currentBranch = tui.getState().branch || getCurrentBranch(cwd);
138
+ const currentPlanningKey = sanitizeBranchForDir(currentBranch);
139
+ const spec = getSpecForBranch(currentBranch, cwd);
140
+ handleAction(tui, action, currentPlanningKey, spec, currentBranch, data);
141
+ },
142
+ onExit: () => {
143
+ console.log('\nExiting All Hands TUI...');
144
+ process.exit(0);
145
+ },
146
+ onSpawnExecutor: (prompt, executorBranch, specId) => {
147
+ spawnExecutorForPrompt(tui, prompt, executorBranch, specId);
148
+ },
149
+ onSpawnEmergentPlanning: (emergentBranch, specId) => {
150
+ spawnEmergentPlanningAgent(tui, emergentBranch, specId);
151
+ },
152
+ cwd: process.cwd(),
153
+ });
154
+
155
+ // Set initial state
156
+ tui.updateState(initialState);
157
+ tui.log(`Branch: ${branch}`);
158
+ if (currentSpec) {
159
+ tui.log(`Spec: ${currentSpec.id} (${status?.stage || 'no planning'})`);
160
+ } else {
161
+ tui.log('No spec for this branch. Use Switch Spec to select one.');
162
+ }
163
+
164
+ // Log TUI launch
165
+ logTuiLifecycle('tui.start', {
166
+ branch,
167
+ spec: currentSpec?.id,
168
+ promptCount: promptItems.length,
169
+ agentCount: activeAgents.length,
170
+ }, cwd);
171
+
172
+ // Start TUI
173
+ tui.start();
174
+ }
175
+
176
+ // No register function - TUI is launched directly from CLI when no command is given
177
+
178
+ /**
179
+ * Spawn agents for a TUI action using profile definitions
180
+ *
181
+ * Looks up all agent profiles with matching tui_action and spawns them.
182
+ * Multiple profiles can share the same tui_action (e.g., compound can spawn multiple agents).
183
+ */
184
+ async function spawnAgentsForAction(
185
+ tui: TUI,
186
+ action: string,
187
+ planningKey: string | null,
188
+ currentSpec: SpecFile | null,
189
+ branch: string,
190
+ status: ReturnType<typeof readStatus>,
191
+ cwd?: string,
192
+ contextOverrides?: Record<string, string>
193
+ ): Promise<boolean> {
194
+ const profileMap = getProfilesByTuiAction();
195
+ const profiles = profileMap.get(action);
196
+
197
+ if (!profiles || profiles.length === 0) {
198
+ return false; // No profiles for this action
199
+ }
200
+
201
+ // Check if any profile requires spec
202
+ const requiresSpec = profiles.some((p) => p.tuiRequiresSpec);
203
+ if (requiresSpec && !currentSpec) {
204
+ tui.log('Error: No spec for this branch. Checkout a spec branch first.');
205
+ return true; // Handled, but with error
206
+ }
207
+
208
+ // Check tmux availability
209
+ if (!isTmuxInstalled()) {
210
+ tui.log('Error: tmux is required for agent spawning');
211
+ return true;
212
+ }
213
+
214
+ // Build template context once for all agents
215
+ const context = buildTemplateContext(
216
+ planningKey || 'default', // Use planning key for paths
217
+ status?.name,
218
+ undefined, // promptNumber - not applicable for TUI actions
219
+ undefined, // promptPath - not applicable for TUI actions
220
+ cwd
221
+ );
222
+
223
+ // Apply context overrides (e.g., WORKFLOW_DOMAIN_PATH for initiative steering)
224
+ if (contextOverrides) {
225
+ Object.assign(context, contextOverrides);
226
+ }
227
+
228
+ // Spawn each profile
229
+ for (const profile of profiles) {
230
+ const label = profile.tuiLabel ?? profile.name;
231
+ tui.log(`Spawning ${label}...`);
232
+
233
+ try {
234
+ const result = spawnAgentFromProfile(
235
+ {
236
+ agentName: profile.name,
237
+ context,
238
+ focusWindow: profiles.length === 1, // Only focus if single agent
239
+ },
240
+ branch,
241
+ cwd
242
+ );
243
+
244
+ tui.log(`Spawned ${profile.name} in ${result.sessionName}:${result.windowName}`);
245
+ } catch (e) {
246
+ const message = e instanceof Error ? e.message : String(e);
247
+ tui.log(`Error spawning ${profile.name}: ${message}`);
248
+ logTuiError('spawn-agent', e instanceof Error ? e : message, {
249
+ action,
250
+ profileName: profile.name,
251
+ spec: currentSpec?.id,
252
+ branch,
253
+ }, cwd);
254
+ }
255
+ }
256
+
257
+ // Track compound_run in status when compound action is triggered
258
+ if (action === 'compound' && planningKey && status) {
259
+ updateStatus({ compound_run: true }, planningKey, cwd);
260
+ }
261
+
262
+ updateRunningAgents(tui, branch);
263
+ return true;
264
+ }
265
+
266
+ /**
267
+ * Check for uncommitted changes and prompt the user for confirmation.
268
+ * Returns true if no uncommitted changes or user confirms proceeding.
269
+ */
270
+ async function confirmProceedWithUncommittedChanges(
271
+ tui: TUI,
272
+ cwd: string,
273
+ message: string
274
+ ): Promise<boolean> {
275
+ if (hasUncommittedChanges(cwd)) {
276
+ const proceed = await tui.showConfirmation('Uncommitted Changes', message);
277
+ return proceed;
278
+ }
279
+ return true;
280
+ }
281
+
282
+ async function handleAction(
283
+ tui: TUI,
284
+ action: string,
285
+ planningKey: string | null,
286
+ currentSpec: SpecFile | null,
287
+ branch: string,
288
+ data?: Record<string, unknown>
289
+ ): Promise<void> {
290
+ const cwd = process.cwd();
291
+
292
+ // Log TUI action
293
+ logTuiAction(action, {
294
+ spec: currentSpec?.id,
295
+ branch,
296
+ planningKey,
297
+ data,
298
+ }, cwd);
299
+
300
+ // Read status from planning directory
301
+ const status = planningKey ? readStatus(planningKey, cwd) : null;
302
+
303
+ // Prepare context overrides for actions that need them
304
+ let contextOverrides: Record<string, string> | undefined;
305
+ if (action === 'initiative-steering' && data?.domain) {
306
+ const selectedDomain = data.domain as string;
307
+ const domainConfigPath = join(cwd, '.allhands', 'workflows', `${selectedDomain}.md`);
308
+ if (existsSync(domainConfigPath)) {
309
+ contextOverrides = {
310
+ WORKFLOW_DOMAIN_PATH: domainConfigPath,
311
+ };
312
+ } else {
313
+ console.warn(`Workflow domain config not found: ${domainConfigPath}`);
314
+ }
315
+ }
316
+
317
+ // Pre-spawn gate: sync with origin/main before compounding
318
+ if (action === 'compound') {
319
+ const syncResult = syncWithOriginMain(cwd);
320
+ if (!syncResult.success && syncResult.conflicts.length > 0) {
321
+ // Merge conflicts — already aborted by syncWithOriginMain
322
+ await tui.showConfirmation(
323
+ 'Compounding Aborted',
324
+ 'Merge conflicts with main — compounding aborted',
325
+ 'Conflicting files:\n' + syncResult.conflicts.map(f => ' - ' + f).join('\n') + '\n\nResolve conflicts manually, push, and retry compounding.'
326
+ );
327
+ return;
328
+ } else if (!syncResult.success) {
329
+ // Fetch failure (no conflicts — network/remote issue)
330
+ await tui.showConfirmation(
331
+ 'Compounding Aborted',
332
+ 'Failed to sync with remote main — compounding aborted',
333
+ 'Could not merge origin/main. This can be caused by uncommitted changes or network issues. Please resolve and try again.'
334
+ );
335
+ return;
336
+ }
337
+ }
338
+
339
+ // Try to handle as a profile-based agent spawn
340
+ const handledByProfile = await spawnAgentsForAction(tui, action, planningKey, currentSpec, branch, status, cwd, contextOverrides);
341
+ if (handledByProfile) {
342
+ return;
343
+ }
344
+
345
+ // Handle non-agent actions
346
+ switch (action) {
347
+ case 'create-pr': {
348
+ if (!currentSpec || !planningKey) {
349
+ tui.log('Error: No spec for this branch. Checkout a spec branch first.');
350
+ return;
351
+ }
352
+
353
+ // Warn about uncommitted changes — gives user a chance to cancel and commit first
354
+ const proceedWithPR = await confirmProceedWithUncommittedChanges(
355
+ tui, cwd, 'You have uncommitted changes that will not be included in the PR. Proceed anyway?'
356
+ );
357
+ if (!proceedWithPR) break;
358
+
359
+ // Check if we're creating or updating
360
+ const existingStatus = status?.pr?.url ? 'Updating' : 'Creating';
361
+ tui.log(`${existingStatus} PR via oracle...`);
362
+
363
+ try {
364
+ // buildPR uses planning key for reading prompts/alignment
365
+ const result = await buildPR(planningKey, cwd);
366
+
367
+ if (result.success && result.prUrl) {
368
+ const action = result.existingPR ? 'updated' : 'created';
369
+ tui.log(`PR ${action}: ${result.prUrl}`);
370
+ tui.setPRUrl(result.prUrl);
371
+
372
+ // Set lastReviewRunTime to now for comment filtering
373
+ updatePRReviewStatus(
374
+ { lastReviewRunTime: new Date().toISOString() },
375
+ planningKey,
376
+ cwd
377
+ );
378
+ } else {
379
+ tui.log(`Error: ${result.body}`);
380
+ tui.log('You may need to push your branch first or check gh auth status.');
381
+ logTuiError('create-pr', result.body || 'PR creation failed', {
382
+ spec: currentSpec.id,
383
+ branch,
384
+ }, cwd);
385
+ }
386
+ } catch (e) {
387
+ const message = e instanceof Error ? e.message : String(e);
388
+ tui.log(`Error: ${message}`);
389
+ logTuiError('create-pr', e instanceof Error ? e : message, {
390
+ spec: currentSpec?.id,
391
+ branch,
392
+ }, cwd);
393
+ }
394
+ break;
395
+ }
396
+
397
+ case 'rerun-pr-review': {
398
+ if (!planningKey) {
399
+ tui.log('Error: No planning context. Checkout a spec branch first.');
400
+ return;
401
+ }
402
+
403
+ // Get PR URL from status
404
+ const prStatus = status?.pr;
405
+ if (!prStatus?.url) {
406
+ tui.log('Error: No PR found. Create a PR first.');
407
+ return;
408
+ }
409
+
410
+ // Warn about uncommitted changes — gives user a chance to cancel and commit first
411
+ const proceedWithReview = await confirmProceedWithUncommittedChanges(
412
+ tui, cwd, 'You have uncommitted changes that will not be included in the PR. Proceed anyway?'
413
+ );
414
+ if (!proceedWithReview) break;
415
+
416
+ tui.log('Triggering PR re-review...');
417
+
418
+ try {
419
+ // Push any unpushed commits before triggering review
420
+ const workingDir = cwd || process.cwd();
421
+ const unpushedResult = gitExec(['log', '@{u}..HEAD', '--oneline'], workingDir);
422
+
423
+ if (unpushedResult.success && unpushedResult.stdout) {
424
+ tui.log('Pushing local commits to remote...');
425
+ const pushResult = gitExec(['push'], workingDir);
426
+ if (pushResult.success) {
427
+ tui.log('Commits pushed successfully.');
428
+ } else {
429
+ tui.log('Warning: Could not push commits. Continuing with review trigger...');
430
+ }
431
+ } else if (!unpushedResult.success) {
432
+ // No upstream or other git error - try push anyway
433
+ const pushResult = gitExec(['push'], workingDir);
434
+ if (!pushResult.success) {
435
+ tui.log('Warning: Could not push commits. Continuing with review trigger...');
436
+ }
437
+ }
438
+
439
+ // Load settings for rerun comment
440
+ const settings = loadProjectSettings();
441
+ const rerunComment = settings?.prReview?.rerunComment ?? '@greptile';
442
+
443
+ // Post comment to trigger review
444
+ const result = await triggerPRReview(prStatus.url, rerunComment, cwd);
445
+
446
+ if (result.success) {
447
+ tui.log(`Re-review triggered with comment: ${rerunComment}`);
448
+
449
+ // Update lastReviewRunTime and transition to awaiting state
450
+ updatePRReviewStatus(
451
+ { lastReviewRunTime: new Date().toISOString() },
452
+ planningKey,
453
+ cwd
454
+ );
455
+
456
+ // Transition TUI back to awaiting-review state
457
+ tui.setPRUrl(prStatus.url);
458
+ } else {
459
+ tui.log('Error: Failed to post review comment');
460
+ logTuiError('rerun-pr-review', 'Failed to post review comment', {
461
+ prUrl: prStatus.url,
462
+ }, cwd);
463
+ }
464
+ } catch (e) {
465
+ const message = e instanceof Error ? e.message : String(e);
466
+ tui.log(`Error: ${message}`);
467
+ logTuiError('rerun-pr-review', e instanceof Error ? e : message, {
468
+ prUrl: prStatus?.url,
469
+ }, cwd);
470
+ }
471
+ break;
472
+ }
473
+
474
+ case 'mark-completed': {
475
+ if (!currentSpec) {
476
+ tui.log('Error: No spec for this branch. Checkout a spec branch first.');
477
+ return;
478
+ }
479
+
480
+ if (currentSpec.status === 'completed') {
481
+ tui.log('Spec is already marked as completed.');
482
+ break;
483
+ }
484
+
485
+ // Warn about uncommitted changes — gives user a chance to cancel and commit first
486
+ const proceedWithComplete = await confirmProceedWithUncommittedChanges(
487
+ tui, cwd, 'You have uncommitted changes that will not be included in the final push. Proceed anyway?'
488
+ );
489
+ if (!proceedWithComplete) break;
490
+
491
+ tui.log(`Marking spec as completed: ${currentSpec.id}`);
492
+
493
+ try {
494
+ /** Remote sync: fetch + merge origin/main */
495
+ tui.log('Syncing with origin/main...');
496
+ const syncResult = syncWithOriginMain(cwd);
497
+
498
+ if (!syncResult.success && syncResult.conflicts.length > 0) {
499
+ // Merge conflicts detected — already aborted by syncWithOriginMain
500
+ const conflictDetail = "Conflicting files:\n" + syncResult.conflicts.map(f => " - " + f).join("\n") + "\n\nResolve conflicts manually, push, and retry completion.";
501
+ await tui.showConfirmation(
502
+ 'Merge Conflicts Detected',
503
+ `Could not merge origin/main into ${branch}.`,
504
+ conflictDetail
505
+ );
506
+ logTuiError('mark-completed', `Merge conflicts: ${syncResult.conflicts.join(', ')}`, {
507
+ spec: currentSpec.id,
508
+ branch,
509
+ conflicts: syncResult.conflicts,
510
+ }, cwd);
511
+ return;
512
+ } else if (!syncResult.success) {
513
+ await tui.showConfirmation(
514
+ 'Completion Aborted',
515
+ 'Failed to sync with remote main — completion aborted.',
516
+ 'Could not merge origin/main. This can be caused by uncommitted changes or network issues. Please resolve and try again.'
517
+ );
518
+ return;
519
+ } else {
520
+ tui.log('Synced with origin/main successfully.');
521
+ }
522
+
523
+ // Compute relative paths for git staging before the move
524
+ const oldRelPath = relative(cwd, currentSpec.path);
525
+ const newRelPath = relative(cwd, join(cwd, 'specs', currentSpec.filename));
526
+ const destPath = join(cwd, 'specs', currentSpec.filename);
527
+
528
+ if (existsSync(destPath)) {
529
+ tui.log(`Error: Destination already exists: ${destPath}`);
530
+ break;
531
+ }
532
+
533
+ // Move spec file from specs/roadmap/ to specs/
534
+ renameSync(currentSpec.path, destPath);
535
+ tui.log(`Moved spec to: specs/${currentSpec.filename}`);
536
+
537
+ // Update frontmatter on the new path
538
+ updateSpecStatus(destPath, 'completed');
539
+
540
+ // Reindex roadmap and docs indexes after file move
541
+ await reindexAfterMove(cwd, currentSpec.path, destPath, true);
542
+
543
+ // Stage only the moved spec file (deletion of old + addition of new)
544
+ gitExec(['add', '--', oldRelPath], cwd);
545
+ gitExec(['add', '--', newRelPath], cwd);
546
+
547
+ // Commit
548
+ const commitResult = gitExec(['commit', '-m', `chore: mark spec ${currentSpec.id} as completed`], cwd);
549
+ if (!commitResult.success) {
550
+ tui.log(`Error committing: ${commitResult.stderr}`);
551
+ break;
552
+ }
553
+
554
+ // Push with -u to ensure upstream tracking is set
555
+ const pushResult = gitExec(['push', '-u', 'origin', 'HEAD'], cwd);
556
+ if (!pushResult.success) {
557
+ tui.log('Warning: Could not push completion commit.');
558
+ }
559
+
560
+ // Refresh spec list to reflect completed status — developer stays on feature branch
561
+ const refreshedSpec = getSpecForBranch(branch, cwd);
562
+ tui.syncBranchContext(branch, refreshedSpec);
563
+
564
+ tui.log(`Spec ${currentSpec.id} completed successfully.`);
565
+ } catch (e) {
566
+ const message = e instanceof Error ? e.message : String(e);
567
+ tui.log(`Error: ${message}`);
568
+ logTuiError('mark-completed', e instanceof Error ? e : message, {
569
+ spec: currentSpec?.id,
570
+ branch,
571
+ }, cwd);
572
+ }
573
+ break;
574
+ }
575
+
576
+ case 'switch-spec': {
577
+ const specId = data?.specId as string | undefined;
578
+ if (!specId || specId.startsWith('header-') || specId === 'info') {
579
+ // Header or info item selected, ignore
580
+ break;
581
+ }
582
+
583
+ tui.log(`Switching to spec: ${specId}`);
584
+
585
+ try {
586
+ /** Guard: check for uncommitted changes before any git operations */
587
+ const proceedWithSwitch = await confirmProceedWithUncommittedChanges(
588
+ tui, cwd, 'You have uncommitted changes that may be lost during branch switch. Proceed anyway?'
589
+ );
590
+ if (!proceedWithSwitch) break;
591
+
592
+ // Find spec file using specs library
593
+ const specFile = findSpecById(specId, cwd);
594
+
595
+ if (!specFile) {
596
+ tui.log(`Error: Spec file not found: ${specId}`);
597
+ break;
598
+ }
599
+
600
+ // Check if spec has a branch assigned
601
+ if (!specFile.branch) {
602
+ tui.log(`Error: Spec "${specId}" has no branch assigned.`);
603
+ tui.log('Use "ah specs create <spec_path>" to assign a branch.');
604
+ break;
605
+ }
606
+
607
+ // Completed specs cannot be switched to
608
+ if (specFile.status === 'completed') {
609
+ tui.log(`Spec "${specFile.id}" is completed and cannot be selected.`);
610
+ break;
611
+ }
612
+
613
+ const specBranch = specFile.branch!;
614
+
615
+ // Validate branch name safety
616
+ validateGitRef(specBranch, 'spec branch');
617
+
618
+ /** Cross-worktree detection: check if spec is active in another worktree */
619
+ const worktreeResult = gitExec(['worktree', 'list', '--porcelain'], cwd);
620
+ if (worktreeResult.success) {
621
+ const worktreeLines = worktreeResult.stdout.split('\n');
622
+ const worktreePaths: string[] = [];
623
+ for (const line of worktreeLines) {
624
+ if (line.startsWith('worktree ')) {
625
+ worktreePaths.push(line.substring('worktree '.length));
626
+ }
627
+ }
628
+
629
+ const sanitizedBranchKey = sanitizeBranchForDir(specBranch);
630
+ for (const wtPath of worktreePaths) {
631
+ // Skip the current working directory
632
+ if (wtPath === cwd) continue;
633
+ const planningPath = join(wtPath, '.planning', sanitizedBranchKey);
634
+ if (existsSync(planningPath)) {
635
+ await tui.showConfirmation(
636
+ 'Spec Active in Another Worktree',
637
+ `Cannot activate spec here.`,
638
+ `Spec '${specFile.id}' is already active in another worktree:\n ${wtPath}\n\nSwitch to that directory to continue work on this spec.`
639
+ );
640
+ process.stderr.write(`Error: Spec '${specFile.id}' is already active in worktree: ${wtPath}\n`);
641
+ logTuiError('switch-spec', `Spec active in another worktree: ${wtPath}`, {
642
+ specId,
643
+ specBranch,
644
+ worktreePath: wtPath,
645
+ }, cwd);
646
+ // Use a flag to break out of the switch case
647
+ tui.log(`Spec is active in worktree: ${wtPath}`);
648
+ return;
649
+ }
650
+ }
651
+ }
652
+
653
+ // Branch creation / checkout with remote sync
654
+ const branchExists = gitExec(['rev-parse', '--verify', specBranch], cwd);
655
+
656
+ if (!branchExists.success) {
657
+ // Branch does NOT exist locally — create from origin/main
658
+ tui.log(`Creating new branch from origin/main: ${specBranch}`);
659
+
660
+ // Fetch first (non-blocking on failure)
661
+ const fetchResult = gitExec(['fetch', 'origin', 'main'], cwd);
662
+ if (!fetchResult.success) {
663
+ tui.log('Warning: Could not fetch from origin. Creating branch from local state.');
664
+ }
665
+
666
+ // Create branch from origin/main (or local main if fetch failed)
667
+ const createBase = fetchResult.success ? 'origin/main' : 'main';
668
+ const createResult = gitExec(['checkout', '-b', specBranch, createBase], cwd);
669
+ if (!createResult.success) {
670
+ tui.log(`Error creating branch: ${createResult.stderr}`);
671
+ logTuiError('checkout-branch', createResult.stderr, {
672
+ specId,
673
+ specBranch,
674
+ branch,
675
+ }, cwd);
676
+ break;
677
+ }
678
+ } else {
679
+ // Branch exists — checkout and merge origin/main
680
+ tui.log(`Checking out existing branch: ${specBranch}`);
681
+
682
+ const checkoutResult = gitExec(['checkout', specBranch], cwd);
683
+ if (!checkoutResult.success) {
684
+ tui.log(`Error checking out branch: ${checkoutResult.stderr}`);
685
+ logTuiError('checkout-branch', checkoutResult.stderr, {
686
+ specId,
687
+ specBranch,
688
+ branch,
689
+ }, cwd);
690
+ break;
691
+ }
692
+
693
+ // Sync with origin/main
694
+ tui.log('Syncing with origin/main...');
695
+ const syncResult = syncWithOriginMain(cwd);
696
+
697
+ if (!syncResult.success && syncResult.conflicts.length > 0) {
698
+ // Merge conflicts detected — already aborted by syncWithOriginMain
699
+ const conflictDetail = "Conflicting files:\n" + syncResult.conflicts.map(f => " - " + f).join("\n") + "\n\nResolve conflicts manually and retry.";
700
+ await tui.showConfirmation(
701
+ 'Merge Conflicts Detected',
702
+ `Could not merge origin/main into ${specBranch}.`,
703
+ conflictDetail
704
+ );
705
+ process.stderr.write(`Error: Merge conflicts in ${specBranch}: ${syncResult.conflicts.join(', ')}\n`);
706
+ logTuiError('switch-spec', `Merge conflicts: ${syncResult.conflicts.join(', ')}`, {
707
+ specId,
708
+ specBranch,
709
+ conflicts: syncResult.conflicts,
710
+ }, cwd);
711
+ // Return without completing activation — user must resolve conflicts
712
+ return;
713
+ } else if (!syncResult.success) {
714
+ await tui.showConfirmation(
715
+ 'Switch Aborted',
716
+ 'Failed to sync with remote main — switch aborted.',
717
+ 'Could not merge origin/main. This can be caused by uncommitted changes or network issues. Please resolve and try again.'
718
+ );
719
+ return;
720
+ } else {
721
+ tui.log('Synced with origin/main successfully.');
722
+ }
723
+ }
724
+
725
+ // Preserve .planning/ directory creation
726
+ const newPlanningKey = sanitizeBranchForDir(specBranch);
727
+
728
+ if (!planningDirExists(newPlanningKey, cwd)) {
729
+ tui.log(`Creating .planning/${newPlanningKey}/`);
730
+ ensurePlanningDir(newPlanningKey, cwd);
731
+ initializeStatus(newPlanningKey, specFile.path, specBranch, cwd);
732
+ }
733
+
734
+ // Update TUI state
735
+ const newPrompts = loadAllPrompts(newPlanningKey, cwd);
736
+ tui.updateState({
737
+ spec: specFile.id,
738
+ branch: specBranch,
739
+ prompts: newPrompts.map((p) => ({
740
+ number: p.frontmatter.number,
741
+ title: p.frontmatter.title,
742
+ status: p.frontmatter.status as 'pending' | 'in_progress' | 'done',
743
+ path: p.path,
744
+ })),
745
+ });
746
+
747
+ // Sync EventLoop state to prevent stale branch detection
748
+ tui.syncBranchContext(specBranch, specFile);
749
+
750
+ tui.log(`Switched to spec: ${specFile.id} on branch: ${specBranch}`);
751
+ } catch (e) {
752
+ const message = e instanceof Error ? e.message : String(e);
753
+ tui.log(`Error: ${message}`);
754
+ logTuiError('switch-spec', e instanceof Error ? e : message, {
755
+ specId: data?.specId,
756
+ spec: currentSpec?.id,
757
+ branch,
758
+ }, cwd);
759
+ }
760
+ break;
761
+ }
762
+
763
+ case 'toggle-loop': {
764
+ const enabled = data?.enabled as boolean;
765
+ // Don't persist enabled state - loop always starts disabled
766
+ tui.log(`Loop: ${enabled ? 'Started' : 'Stopped'}`);
767
+ break;
768
+ }
769
+
770
+ case 'toggle-parallel': {
771
+ const enabled = data?.enabled as boolean;
772
+ if (planningKey && status) {
773
+ updateStatus({ loop: { ...status.loop, parallel: enabled } }, planningKey, cwd);
774
+ }
775
+ tui.log(`Parallel: ${enabled ? 'Enabled' : 'Disabled'}`);
776
+ break;
777
+ }
778
+
779
+ case 'select-prompt': {
780
+ const prompt = data?.prompt as { number: number; title: string } | undefined;
781
+ if (prompt) {
782
+ tui.log(`Selected prompt: ${prompt.number}. ${prompt.title}`);
783
+ // TODO: Could show prompt details or offer to edit
784
+ }
785
+ break;
786
+ }
787
+
788
+ case 'refresh': {
789
+ // Reload prompts from filesystem
790
+ if (planningKey && planningDirExists(planningKey, cwd)) {
791
+ const refreshedPrompts = loadAllPrompts(planningKey, cwd);
792
+ tui.updateState({
793
+ prompts: refreshedPrompts.map((p) => ({
794
+ number: p.frontmatter.number,
795
+ title: p.frontmatter.title,
796
+ status: p.frontmatter.status as 'pending' | 'in_progress' | 'done',
797
+ path: p.path,
798
+ })),
799
+ });
800
+ tui.log(`Refreshed: ${refreshedPrompts.length} prompts`);
801
+ } else {
802
+ tui.log('No planning directory for this branch');
803
+ }
804
+ break;
805
+ }
806
+
807
+ case 'new-initiative': {
808
+ const specType = data?.specType as string | undefined;
809
+ if (!specType) {
810
+ tui.log('Error: No spec type selected.');
811
+ break;
812
+ }
813
+
814
+ tui.log(`New Initiative: ${specType}`);
815
+
816
+ // Spawn ideation agent with unified scoping flow and domain-specific config
817
+ try {
818
+ const context = buildTemplateContext(
819
+ planningKey || 'default',
820
+ status?.name,
821
+ undefined,
822
+ undefined,
823
+ cwd
824
+ );
825
+
826
+ // Override WORKFLOW_DOMAIN_PATH based on the selected spec type
827
+ context.WORKFLOW_DOMAIN_PATH = join(cwd, '.allhands', 'workflows', `${specType}.md`);
828
+
829
+ // Detect active spec for revision mode
830
+ const activeSpec = getSpecForBranch(branch, cwd);
831
+ if (activeSpec && activeSpec.status !== 'completed') {
832
+ const specAbsPath = activeSpec.path.startsWith('/') ? activeSpec.path : join(cwd, activeSpec.path);
833
+ context.SPEC_PATH = specAbsPath;
834
+ context.SPEC_NAME = activeSpec.id;
835
+ tui.log(`Active spec detected: ${activeSpec.id} — ideation will enter revision mode`);
836
+ }
837
+
838
+ // All spec types route to the unified scoping flow
839
+ const flowOverride = join(getFlowsDirectory(), UNIFIED_SCOPING_FLOW);
840
+
841
+ const result = spawnAgentFromProfile(
842
+ {
843
+ agentName: 'ideation',
844
+ context,
845
+ focusWindow: true,
846
+ flowOverride,
847
+ },
848
+ branch,
849
+ cwd
850
+ );
851
+
852
+ tui.log(`Spawned ideation in ${result.sessionName}:${result.windowName}`);
853
+ updateRunningAgents(tui, branch);
854
+ } catch (e) {
855
+ const message = e instanceof Error ? e.message : String(e);
856
+ tui.log(`Error spawning ideation: ${message}`);
857
+ logTuiError('new-initiative', e instanceof Error ? e : message, {
858
+ specType,
859
+ branch,
860
+ }, cwd);
861
+ }
862
+ break;
863
+ }
864
+
865
+ default:
866
+ tui.log(`Action: ${action}`);
867
+ }
868
+ }
869
+
870
+ function updateRunningAgents(tui: TUI, branch: string): void {
871
+ const agents = getRunningAgents(branch);
872
+ const activeAgents: AgentInfo[] = agents.map((a) => ({
873
+ name: a.windowName,
874
+ agentType: a.agentType || 'unknown',
875
+ isRunning: true,
876
+ }));
877
+ tui.updateState({ activeAgents });
878
+ }
879
+
880
+ function spawnExecutorForPrompt(tui: TUI, prompt: PromptFile, branch: string, specId: string): void {
881
+ const promptNumber = prompt.frontmatter.number;
882
+ const cwd = process.cwd();
883
+ const planningKey = sanitizeBranchForDir(branch);
884
+
885
+ tui.log(`Spawning executor for: ${prompt.frontmatter.title}`);
886
+
887
+ try {
888
+ // Build context with prompt-specific info (use sanitized planning key for paths)
889
+ const context = buildTemplateContext(
890
+ planningKey,
891
+ specId, // Use spec file name, not prompt title
892
+ promptNumber,
893
+ prompt.path,
894
+ cwd
895
+ );
896
+
897
+ const result = spawnAgentFromProfile(
898
+ {
899
+ agentName: 'executor',
900
+ context,
901
+ promptNumber,
902
+ focusWindow: false, // Don't steal focus from TUI
903
+ },
904
+ branch,
905
+ cwd
906
+ );
907
+
908
+ tui.log(`Spawned executor in ${result.sessionName}:${result.windowName}`);
909
+ updateRunningAgents(tui, branch);
910
+ } catch (e) {
911
+ const message = e instanceof Error ? e.message : String(e);
912
+ tui.log(`Error spawning executor: ${message}`);
913
+ logTuiError('spawn-executor', e instanceof Error ? e : message, {
914
+ promptNumber,
915
+ promptTitle: prompt.frontmatter.title,
916
+ branch,
917
+ }, cwd);
918
+ }
919
+ }
920
+
921
+ function spawnEmergentPlanningAgent(tui: TUI, branch: string, specId: string): void {
922
+ const cwd = process.cwd();
923
+ const planningKey = sanitizeBranchForDir(branch);
924
+
925
+ // Get next available prompt number for emergent planner window name
926
+ const nextPromptNumber = getNextPromptNumber(planningKey, cwd);
927
+
928
+ tui.log(`Spawning emergent planner (will create prompts from ${nextPromptNumber})`);
929
+
930
+ try {
931
+ const context = buildTemplateContext(
932
+ planningKey,
933
+ specId,
934
+ nextPromptNumber,
935
+ undefined,
936
+ cwd
937
+ );
938
+
939
+ const result = spawnAgentFromProfile(
940
+ {
941
+ agentName: 'emergent',
942
+ context,
943
+ promptNumber: nextPromptNumber,
944
+ focusWindow: false, // Don't steal focus from TUI
945
+ },
946
+ branch,
947
+ cwd
948
+ );
949
+
950
+ tui.log(`Spawned emergent planner in ${result.sessionName}:${result.windowName}`);
951
+ updateRunningAgents(tui, branch);
952
+ } catch (e) {
953
+ const message = e instanceof Error ? e.message : String(e);
954
+ tui.log(`Error spawning emergent planner: ${message}`);
955
+ logTuiError('spawn-emergent', e instanceof Error ? e : message, {
956
+ promptNumber: nextPromptNumber,
957
+ branch,
958
+ }, cwd);
959
+ }
960
+ }