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,1574 @@
1
+ /**
2
+ * TUI - Terminal User Interface for All Hands
3
+ *
4
+ * Three-pane layout:
5
+ * - Actions Pane (left): Agent spawners, toggles, quit/refresh
6
+ * - Prompt List Pane (center): Prompts by status
7
+ * - Status Pane (right): Active agents grid
8
+ *
9
+ * Navigation:
10
+ * - Tab/Shift-Tab: Cycle panes
11
+ * - j/k: Navigate within pane
12
+ * - u/d: Page up/down
13
+ * - Space: Toggle/select
14
+ * - Esc: Close modals
15
+ */
16
+
17
+ import blessed from 'blessed';
18
+ import { join } from 'path';
19
+ import { loadProjectSettings } from '../hooks/shared.js';
20
+ import { CLIDaemon } from '../lib/cli-daemon.js';
21
+ import { validateDocsAsync } from '../lib/docs-validation.js';
22
+ import { EventLoop } from '../lib/event-loop.js';
23
+ import { flowsToModalItems, loadAllFlows } from '../lib/flows.js';
24
+ import { KnowledgeService, reindexAllInWorker, reindexFromChangesInWorker } from '../lib/knowledge.js';
25
+ import { loadAllProfiles } from '../lib/opencode/index.js';
26
+ import { planningDirExists, readStatus, sanitizeBranchForDir } from '../lib/planning.js';
27
+ import { loadAllPrompts, type PromptFile } from '../lib/prompts.js';
28
+ import { clearTuiSession, getHubWindowId, getSpawnedWindows } from '../lib/session.js';
29
+ import { getSpecForBranch, getWorkflowDomain, loadAllSpecs, specsToModalItems, type SpecFile } from '../lib/specs.js';
30
+ import { buildSemanticIndexAsync, ensureTldrDaemon, hasSemanticIndex, isTldrInstalled, needsSemanticRebuild, warmCallGraph } from '../lib/tldr.js';
31
+ import { getCurrentSession, killWindow, listWindows, spawnCustomFlow } from '../lib/tmux.js';
32
+ import { clearLogs, logTuiError, logTuiLifecycle } from '../lib/trace-store.js';
33
+ import { ActionItem, createActionsPane, ToggleState } from './actions.js';
34
+ import { createFileViewer, FileViewer, getPlanningFilePath, getSpecFilePath } from './file-viewer-modal.js';
35
+ import { createModal, Modal } from './modal.js';
36
+ import { createPromptsPane, PromptItem } from './prompts-pane.js';
37
+ import { AgentInfo, createStatusPane, getSelectableItems } from './status-pane.js';
38
+
39
+ /**
40
+ * Shared workflow domain items for initiative and steering modals.
41
+ * Single source of truth for the 6 domain {id, label} pairs.
42
+ */
43
+ export const WORKFLOW_DOMAIN_ITEMS: ReadonlyArray<{ id: string; label: string }> = [
44
+ { id: 'milestone', label: 'Milestone — Feature development with deep ideation' },
45
+ { id: 'investigation', label: 'Investigation — Debug / diagnose issues' },
46
+ { id: 'optimization', label: 'Optimization — Performance / efficiency work' },
47
+ { id: 'refactor', label: 'Refactor — Cleanup / tech debt' },
48
+ { id: 'documentation', label: 'Documentation — Coverage gaps' },
49
+ { id: 'triage', label: 'Triage — External signal analysis' },
50
+ ];
51
+
52
+ export type PaneId = 'actions' | 'prompts' | 'status';
53
+
54
+ export type PRActionState = 'create-pr' | 'awaiting-review' | 'rerun-pr-review';
55
+
56
+ export interface TUIOptions {
57
+ onAction: (action: string, data?: Record<string, unknown>) => void;
58
+ onExit: () => void;
59
+ onSpawnExecutor?: (prompt: PromptFile, branch: string, specId: string) => void;
60
+ onSpawnEmergentPlanning?: (branch: string, specId: string) => void;
61
+ cwd?: string;
62
+ }
63
+
64
+ export interface TUIState {
65
+ loopEnabled: boolean;
66
+ parallelEnabled: boolean;
67
+ prompts: PromptItem[];
68
+ activeAgents: AgentInfo[];
69
+ spec?: string;
70
+ branch?: string;
71
+ baseBranch?: string;
72
+ prActionState: PRActionState;
73
+ customFlowCounter: number;
74
+ }
75
+
76
+ export class TUI {
77
+ private screen: blessed.Widgets.Screen;
78
+ private actionsPane: blessed.Widgets.BoxElement;
79
+ private promptsPane: blessed.Widgets.BoxElement;
80
+ private statusPane: blessed.Widgets.BoxElement;
81
+
82
+ private state: TUIState;
83
+ private options: TUIOptions;
84
+
85
+ // Navigation state
86
+ private focusedPane: PaneId = 'actions';
87
+ private paneOrder: PaneId[] = ['actions', 'prompts', 'status'];
88
+ private selectedIndex: Record<PaneId, number> = {
89
+ actions: 0,
90
+ prompts: 0,
91
+ status: 0,
92
+ };
93
+
94
+ // Modals
95
+ private activeModal: Modal | null = null;
96
+ private activeFileViewer: FileViewer | null = null;
97
+ private logEntries: string[] = [];
98
+
99
+ // Action items (for selection tracking)
100
+ private actionItems: ActionItem[] = [];
101
+
102
+ // Event loop daemon
103
+ private eventLoop: EventLoop | null = null;
104
+
105
+ // CLI daemon for fast hook execution
106
+ private cliDaemon: CLIDaemon | null = null;
107
+
108
+ // Original output functions for restoration on destroy
109
+ private originalStdoutWrite: typeof process.stdout.write | null = null;
110
+ private originalStderrWrite: typeof process.stderr.write | null = null;
111
+ private originalConsoleLog: typeof console.log | null = null;
112
+ private originalConsoleError: typeof console.error | null = null;
113
+
114
+ constructor(options: TUIOptions) {
115
+ this.options = options;
116
+ this.state = {
117
+ loopEnabled: false,
118
+ parallelEnabled: false,
119
+ prompts: [],
120
+ activeAgents: [],
121
+ prActionState: 'create-pr',
122
+ customFlowCounter: 0,
123
+ };
124
+
125
+ // Suppress terminal capability errors (e.g., xterm-ghostty.Setulc) during screen creation
126
+ // These errors come from blessed parsing terminfo and can go to stdout/stderr/console
127
+ const originalStdoutWrite = process.stdout.write.bind(process.stdout);
128
+ const originalStderrWrite = process.stderr.write.bind(process.stderr);
129
+ const originalConsoleLog = console.log.bind(console);
130
+ const originalConsoleError = console.error.bind(console);
131
+
132
+ const isTerminfoNoise = (str: string): boolean => {
133
+ return (
134
+ str.includes('Setulc') ||
135
+ str.includes('Error on xterm') ||
136
+ str.includes('stack.push') ||
137
+ str.includes('out.push') ||
138
+ str.includes('stack.pop') ||
139
+ str.includes('stack = []') ||
140
+ str.includes('var v,') ||
141
+ str.includes('return out.join') ||
142
+ /^"\s*\\u001b\[/.test(str) ||
143
+ /^\s*out = \[/.test(str)
144
+ );
145
+ };
146
+
147
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
148
+ (process.stdout as any).write = (chunk: string | Uint8Array, ...args: unknown[]): boolean => {
149
+ const str = typeof chunk === 'string' ? chunk : chunk.toString();
150
+ if (isTerminfoNoise(str)) return true;
151
+ return originalStdoutWrite(chunk, ...(args as [BufferEncoding?, ((err?: Error | null) => void)?]));
152
+ };
153
+
154
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
155
+ (process.stderr as any).write = (chunk: string | Uint8Array, ...args: unknown[]): boolean => {
156
+ const str = typeof chunk === 'string' ? chunk : chunk.toString();
157
+ if (isTerminfoNoise(str)) return true;
158
+ return originalStderrWrite(chunk, ...(args as [BufferEncoding?, ((err?: Error | null) => void)?]));
159
+ };
160
+
161
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
162
+ console.log = (...args: any[]): void => {
163
+ const str = args.map(a => String(a)).join(' ');
164
+ if (isTerminfoNoise(str)) return;
165
+ originalConsoleLog(...args);
166
+ };
167
+
168
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
169
+ console.error = (...args: any[]): void => {
170
+ const str = args.map(a => String(a)).join(' ');
171
+ if (isTerminfoNoise(str)) return;
172
+ originalConsoleError(...args);
173
+ };
174
+
175
+ // Create screen with terminal compatibility options
176
+ this.screen = blessed.screen({
177
+ smartCSR: true,
178
+ title: 'All Hands - Agentic Harness',
179
+ fullUnicode: true,
180
+ warnings: false, // Suppress terminal capability warnings
181
+ });
182
+
183
+ // Store originals for restore on destroy
184
+ this.originalStdoutWrite = originalStdoutWrite;
185
+ this.originalStderrWrite = originalStderrWrite;
186
+ this.originalConsoleLog = originalConsoleLog;
187
+ this.originalConsoleError = originalConsoleError;
188
+
189
+ // Create header (element attaches to screen via parent option)
190
+ this.createHeader();
191
+
192
+ // Create panes
193
+ this.actionsPane = createActionsPane(this.screen, this.getToggleState());
194
+ this.promptsPane = createPromptsPane(this.screen, this.state.prompts);
195
+ this.statusPane = createStatusPane(
196
+ this.screen,
197
+ this.state.activeAgents,
198
+ undefined,
199
+ this.state.spec,
200
+ this.state.branch,
201
+ this.state.baseBranch,
202
+ this.logEntries,
203
+ undefined, // fileStates - will be set on render
204
+ undefined // options - will be set on render
205
+ );
206
+
207
+ // Build action items list for navigation
208
+ this.buildActionItems();
209
+
210
+ // Setup navigation
211
+ this.setupKeyBindings();
212
+
213
+ // Initialize event loop daemon
214
+ if (options.cwd) {
215
+ this.eventLoop = new EventLoop(options.cwd, {
216
+ onPRReviewFeedback: (available: boolean) => {
217
+ if (available && this.state.prActionState === 'awaiting-review') {
218
+ this.state.prActionState = 'rerun-pr-review';
219
+ this.buildActionItems();
220
+ this.log('PR review feedback available - ready to review or rerun');
221
+ this.render();
222
+ }
223
+ },
224
+ onBranchChange: (newBranch, newSpec) => {
225
+ this.log(`Branch changed to: ${newBranch}`);
226
+
227
+ const updates: Partial<TUIState> = { branch: newBranch };
228
+ const newSpecId = newSpec?.id;
229
+
230
+ if (newSpecId !== this.state.spec) {
231
+ updates.spec = newSpecId;
232
+
233
+ if (this.options.cwd) {
234
+ const planningKey = sanitizeBranchForDir(newBranch);
235
+ if (planningDirExists(planningKey, this.options.cwd)) {
236
+ const prompts = loadAllPrompts(planningKey, this.options.cwd);
237
+ const status = readStatus(planningKey, this.options.cwd);
238
+
239
+ updates.prompts = prompts.map((p: { path: string; frontmatter: { number: number; title: string; status: string } }) => ({
240
+ number: p.frontmatter.number,
241
+ title: p.frontmatter.title,
242
+ status: p.frontmatter.status as 'pending' | 'in_progress' | 'done',
243
+ path: p.path,
244
+ }));
245
+ // Don't restore loopEnabled from status - always requires manual enable
246
+ this.state.parallelEnabled = status?.loop?.parallel ?? false;
247
+ } else {
248
+ this.state.prompts = [];
249
+ this.state.loopEnabled = false;
250
+ this.state.parallelEnabled = false;
251
+ }
252
+
253
+ // Sync toggle states to event loop
254
+ this.eventLoop?.setParallelEnabled(this.state.parallelEnabled);
255
+ }
256
+
257
+ if (newSpec) {
258
+ this.log(`Spec: ${newSpec.id}`);
259
+ } else {
260
+ this.log('No spec for this branch');
261
+ }
262
+ }
263
+
264
+ this.updateState(updates);
265
+ },
266
+ onAgentsChange: (agents) => {
267
+ this.state.activeAgents = agents.map((name) => ({
268
+ name,
269
+ agentType: name,
270
+ isRunning: true,
271
+ }));
272
+ this.render();
273
+ },
274
+ onSpawnExecutor: (prompt) => {
275
+ this.log(`Loop: Spawning executor for prompt ${prompt.frontmatter.number}`);
276
+ if (this.state.branch && this.options.onSpawnExecutor) {
277
+ // Use spec if available, otherwise fall back to planning key
278
+ const specId = this.state.spec || sanitizeBranchForDir(this.state.branch);
279
+ this.options.onSpawnExecutor(prompt, this.state.branch, specId);
280
+ }
281
+ },
282
+ onSpawnEmergentPlanning: () => {
283
+ this.log('Loop: Spawning emergent planner');
284
+ if (this.state.branch && this.options.onSpawnEmergentPlanning) {
285
+ const specId = this.state.spec || sanitizeBranchForDir(this.state.branch);
286
+ this.options.onSpawnEmergentPlanning(this.state.branch, specId);
287
+ }
288
+ },
289
+ onLoopStatus: (message) => {
290
+ this.log(`Loop: ${message}`);
291
+ },
292
+ onPromptsChange: (prompts, snapshot) => {
293
+ // Update TUI state when prompts are added, removed, or status changes
294
+ const prevCount = this.state.prompts.length;
295
+ this.state.prompts = prompts.map((p) => ({
296
+ number: p.frontmatter.number,
297
+ title: p.frontmatter.title,
298
+ status: p.frontmatter.status as 'pending' | 'in_progress' | 'done',
299
+ path: p.path,
300
+ }));
301
+
302
+ // Log meaningful changes
303
+ if (snapshot.count !== prevCount) {
304
+ this.log(`Prompts: ${snapshot.count} (${snapshot.pending} pending, ${snapshot.inProgress} in progress, ${snapshot.done} done)`);
305
+ }
306
+
307
+ this.buildActionItems();
308
+ this.render();
309
+ },
310
+ });
311
+ this.eventLoop.start();
312
+
313
+ // Start CLI daemon for fast hook execution (if enabled in settings)
314
+ const settings = loadProjectSettings();
315
+ const daemonEnabled = settings?.daemon?.enabled !== false; // default true
316
+ if (daemonEnabled) {
317
+ this.cliDaemon = new CLIDaemon(options.cwd);
318
+ this.cliDaemon.start().then(() => {
319
+ this.log(`CLI daemon ready (${this.cliDaemon?.getHandlerCount() ?? 0} handlers)`);
320
+ this.render();
321
+ }).catch((e) => {
322
+ this.log(`CLI daemon failed: ${e instanceof Error ? e.message : e}`);
323
+ });
324
+ }
325
+
326
+ // Start background indexing (non-blocking)
327
+ this.startBackgroundIndexing();
328
+ }
329
+
330
+ // Initial render
331
+ this.render();
332
+ }
333
+
334
+ /**
335
+ * Start background indexing of knowledge bases and validation.
336
+ * Non-blocking - progress is logged to status pane.
337
+ */
338
+ private async startBackgroundIndexing(): Promise<void> {
339
+ if (!this.options.cwd) return;
340
+
341
+ this.log('Starting background index...');
342
+ this.render();
343
+
344
+ try {
345
+ // Ensure TLDR daemon is running first (required for dirty file tracking)
346
+ if (isTldrInstalled()) {
347
+ const daemonStarted = await ensureTldrDaemon(this.options.cwd);
348
+ if (daemonStarted) {
349
+ this.log('TLDR daemon ready');
350
+ } else {
351
+ this.log('TLDR daemon failed to start');
352
+ }
353
+ this.render();
354
+
355
+ const needsIndex = !hasSemanticIndex(this.options.cwd);
356
+ const needsRebuild = needsSemanticRebuild(this.options.cwd);
357
+
358
+ if (needsIndex || needsRebuild) {
359
+ this.log(needsIndex ? 'Building semantic index for first run...' : 'Rebuilding semantic index (branch changed)...');
360
+ this.render();
361
+ const result = await buildSemanticIndexAsync(this.options.cwd, (msg) => {
362
+ this.log(msg);
363
+ this.render();
364
+ });
365
+ if (result.success) {
366
+ const langInfo = result.languages.length > 0 ? ` (${result.languages.join(', ')})` : '';
367
+ const countInfo = result.filesIndexed > 0 ? `${result.filesIndexed} files` : '';
368
+ this.log(`Semantic index ready${countInfo ? `: ${countInfo}` : ''}${langInfo} ✓`);
369
+ } else {
370
+ this.log('Semantic index failed');
371
+ }
372
+ this.render();
373
+ }
374
+
375
+ // Always run warm to build/update call graph cache
376
+ this.log('Warming call graph cache...');
377
+ this.render();
378
+ const warmResult = await warmCallGraph(this.options.cwd, (msg) => {
379
+ this.log(msg);
380
+ this.render();
381
+ });
382
+ if (warmResult.success) {
383
+ this.log(`Call graph ready: ${warmResult.files} files, ${warmResult.edges} edges ✓`);
384
+ } else {
385
+ this.log('Call graph warm skipped or failed');
386
+ }
387
+ this.render();
388
+ }
389
+
390
+ // Validate agent profiles first
391
+ this.log('Validating agent profiles...');
392
+ this.render();
393
+ const { profiles, errors: profileErrors } = loadAllProfiles();
394
+ if (profileErrors.length > 0) {
395
+ for (const err of profileErrors) {
396
+ for (const e of err.errors) {
397
+ this.log(`⚠ Agent ${err.name}: ${e}`);
398
+ }
399
+ for (const w of err.warnings) {
400
+ this.log(`⚠ Agent ${err.name}: ${w}`);
401
+ }
402
+ }
403
+ } else {
404
+ this.log(`${profiles.length} agent profiles valid ✓`);
405
+ }
406
+ this.render();
407
+
408
+ // GC hint: reclaim memory from TLDR child process buffers before knowledge indexing
409
+ if (global.gc) {
410
+ global.gc();
411
+ await new Promise((resolve) => setTimeout(resolve, 100));
412
+ }
413
+
414
+ const cwd = this.options.cwd;
415
+ const service = new KnowledgeService(cwd, { quiet: true });
416
+
417
+ // Smart incremental indexing: check if indexes exist before deciding strategy
418
+ // indexExists and getChangesFromGit are lightweight (no model needed) - keep in-process
419
+ const roadmapExists = service.indexExists('roadmap');
420
+ const docsExists = service.indexExists('docs');
421
+
422
+ // Log indexing decision to trace for debugging
423
+ logTuiLifecycle('indexing.start', {
424
+ roadmapExists,
425
+ docsExists,
426
+ strategy: (!roadmapExists || !docsExists) ? 'full' : 'incremental',
427
+ }, cwd);
428
+
429
+ const workerProgress = (msg: string) => {
430
+ this.log(msg);
431
+ this.render();
432
+ };
433
+
434
+ if (!roadmapExists || !docsExists) {
435
+ // Cold start: full index required (via worker process)
436
+ if (!roadmapExists) {
437
+ this.log('Building roadmap index (first run)...');
438
+ logTuiLifecycle('indexing.full', { index: 'roadmap', reason: 'index_missing' }, cwd);
439
+ this.render();
440
+ await reindexAllInWorker(cwd, 'roadmap', workerProgress);
441
+ }
442
+ if (!docsExists) {
443
+ this.log('Building docs index (first run)...');
444
+ logTuiLifecycle('indexing.full', { index: 'docs', reason: 'index_missing' }, cwd);
445
+ this.render();
446
+ await reindexAllInWorker(cwd, 'docs', workerProgress);
447
+ }
448
+ } else {
449
+ // Warm start: incremental update from git changes
450
+ const roadmapChanges = service.getChangesFromGit('roadmap');
451
+ const docsChanges = service.getChangesFromGit('docs');
452
+
453
+ // Log change detection results
454
+ logTuiLifecycle('indexing.changes_detected', {
455
+ roadmapChanges: roadmapChanges.length,
456
+ docsChanges: docsChanges.length,
457
+ roadmapChangeFiles: roadmapChanges.slice(0, 10).map(c => c.path),
458
+ docsChangeFiles: docsChanges.slice(0, 10).map(c => c.path),
459
+ }, cwd);
460
+
461
+ if (roadmapChanges.length > 0) {
462
+ this.log(`Updating roadmap index (${roadmapChanges.length} changes)...`);
463
+ logTuiLifecycle('indexing.incremental', { index: 'roadmap', changeCount: roadmapChanges.length }, cwd);
464
+ this.render();
465
+ await reindexFromChangesInWorker(cwd, 'roadmap', roadmapChanges, workerProgress);
466
+ } else {
467
+ this.log('Roadmap index up to date ✓');
468
+ logTuiLifecycle('indexing.skip', { index: 'roadmap', reason: 'no_changes' }, cwd);
469
+ }
470
+
471
+ if (docsChanges.length > 0) {
472
+ this.log(`Updating docs index (${docsChanges.length} changes)...`);
473
+ logTuiLifecycle('indexing.incremental', { index: 'docs', changeCount: docsChanges.length }, cwd);
474
+ this.render();
475
+ await reindexFromChangesInWorker(cwd, 'docs', docsChanges, workerProgress);
476
+ } else {
477
+ this.log('Docs index up to date ✓');
478
+ logTuiLifecycle('indexing.skip', { index: 'docs', reason: 'no_changes' }, cwd);
479
+ }
480
+ }
481
+
482
+ logTuiLifecycle('indexing.complete', {}, cwd);
483
+
484
+ // Run docs validation
485
+ this.log('Validating documentation...');
486
+ this.render();
487
+ const docsPath = join(cwd, 'docs');
488
+ const excludePaths = ["docs/memories.md", "docs/solutions"].map((p) => join(cwd, p));
489
+ const validation = await validateDocsAsync(docsPath, cwd, { excludePaths });
490
+
491
+ if (validation.frontmatter_error_count > 0) {
492
+ this.log(`⚠ ${validation.frontmatter_error_count} frontmatter errors`);
493
+ for (const err of validation.frontmatter_errors) {
494
+ this.log(` → ${err.doc_file}: ${err.reason}`);
495
+ }
496
+ }
497
+ if (validation.stale_count > 0) {
498
+ this.log(`⚠ ${validation.stale_count} stale references`);
499
+ for (const ref of validation.stale) {
500
+ this.log(` → ${ref.doc_file}: ${ref.reference}`);
501
+ }
502
+ }
503
+ if (validation.invalid_count > 0) {
504
+ this.log(`⚠ ${validation.invalid_count} invalid references`);
505
+ for (const ref of validation.invalid) {
506
+ this.log(` → ${ref.doc_file}: ${ref.reference} (${ref.reason})`);
507
+ }
508
+ }
509
+
510
+ this.log('Index ready ✓');
511
+ this.render();
512
+ } catch (err) {
513
+ const message = err instanceof Error ? err.message : String(err);
514
+ this.log(`Index error: ${message}`);
515
+ logTuiError('backgroundIndexing', err instanceof Error ? err : message, {
516
+ spec: this.state.spec,
517
+ branch: this.state.branch,
518
+ }, this.options.cwd);
519
+ this.render();
520
+ }
521
+ }
522
+
523
+ private createHeader(): blessed.Widgets.BoxElement {
524
+ return blessed.box({
525
+ parent: this.screen,
526
+ top: 0,
527
+ left: 0,
528
+ width: '100%',
529
+ height: 3,
530
+ content: '{center}{bold}{#a78bfa-fg}ALL HANDS{/#a78bfa-fg} {#e0e7ff-fg}AGENTIC HARNESS{/#e0e7ff-fg}{/bold}{/center}',
531
+ tags: true,
532
+ style: {
533
+ fg: '#e0e7ff',
534
+ border: {
535
+ fg: '#4A34C5',
536
+ },
537
+ },
538
+ border: {
539
+ type: 'line',
540
+ },
541
+ });
542
+ }
543
+
544
+ private getToggleState(): ToggleState {
545
+ return {
546
+ loopEnabled: this.state.loopEnabled,
547
+ parallelEnabled: this.state.parallelEnabled,
548
+ prActionState: this.state.prActionState,
549
+ };
550
+ }
551
+
552
+ private buildActionItems(): void {
553
+ this.actionItems = [
554
+ // Agent spawners — all always visible
555
+ { id: 'coordinator', label: 'Coordinator', key: '1', type: 'action' },
556
+ { id: 'new-initiative', label: 'New Initiative', key: '2', type: 'action' },
557
+ { id: 'planner', label: 'Planner', key: '3', type: 'action' },
558
+ { id: 'review-jury', label: 'Review Jury', key: '4', type: 'action' },
559
+ { id: 'e2e-test-planner', label: 'E2E Test Plan', key: '5', type: 'action' },
560
+ { id: 'pr-action', label: this.getPRActionLabel(), key: '6', type: 'action' },
561
+ { id: 'review-pr', label: 'Address PR Review', key: '7', type: 'action' },
562
+ { id: 'compound', label: 'Compound', key: '8', type: 'action' },
563
+ { id: 'mark-completed', label: 'Complete', key: '9', type: 'action' },
564
+ { id: 'switch-spec', label: 'Switch Workspace', key: '0', type: 'action' },
565
+ { id: 'custom-flow', label: 'Custom Flow', key: '-', type: 'action' },
566
+ { id: 'initiative-steering', label: 'Steer Initiative', key: '=', type: 'action' },
567
+ { id: 'separator-toggles', label: '─ Toggles ─', type: 'separator' },
568
+ { id: 'toggle-loop', label: 'Loop', key: 'O', type: 'toggle', checked: this.state.loopEnabled },
569
+ { id: 'toggle-parallel', label: 'Parallel', key: 'P', type: 'toggle', checked: this.state.parallelEnabled },
570
+ { id: 'separator-controls', label: '─ Controls ─', type: 'separator' },
571
+ { id: 'view-logs', label: 'View Logs', key: 'V', type: 'action' },
572
+ { id: 'clear-logs', label: 'Clear Logs', key: 'C', type: 'action' },
573
+ { id: 'refresh', label: 'Refresh', key: 'R', type: 'action' },
574
+ { id: 'quit', label: 'Quit', key: 'Q', type: 'action' },
575
+ ];
576
+ }
577
+
578
+ private getPRActionLabel(): string {
579
+ switch (this.state.prActionState) {
580
+ case 'create-pr': return 'Create PR';
581
+ case 'awaiting-review': return 'Awaiting Review...';
582
+ case 'rerun-pr-review': return 'Rerun PR Review';
583
+ }
584
+ }
585
+
586
+ private getSelectableActionItems(): ActionItem[] {
587
+ return this.actionItems.filter(item => item.type !== 'separator');
588
+ }
589
+
590
+ private setupKeyBindings(): void {
591
+ // Quit on Ctrl-C
592
+ this.screen.key(['C-c'], () => {
593
+ this.handleAction('quit');
594
+ });
595
+
596
+ // Tab/Shift-Tab for pane cycling
597
+ this.screen.key(['tab'], () => {
598
+ this.cyclePane(1);
599
+ });
600
+ this.screen.key(['S-tab'], () => {
601
+ this.cyclePane(-1);
602
+ });
603
+
604
+ // Vim navigation within panes
605
+ this.screen.key(['j'], () => {
606
+ if (!this.activeModal && !this.activeFileViewer) {
607
+ this.navigatePane(1);
608
+ }
609
+ });
610
+ this.screen.key(['k'], () => {
611
+ if (!this.activeModal && !this.activeFileViewer) {
612
+ this.navigatePane(-1);
613
+ }
614
+ });
615
+ this.screen.key(['u'], () => {
616
+ if (!this.activeModal && !this.activeFileViewer) {
617
+ this.navigatePane(-10); // Page up
618
+ }
619
+ });
620
+ this.screen.key(['d'], () => {
621
+ if (!this.activeModal && !this.activeFileViewer) {
622
+ this.navigatePane(10); // Page down
623
+ }
624
+ });
625
+
626
+ // Space to select/toggle
627
+ this.screen.key(['space'], () => {
628
+ if (!this.activeModal && !this.activeFileViewer) {
629
+ this.selectCurrentItem();
630
+ }
631
+ });
632
+
633
+ // Enter to activate
634
+ this.screen.key(['enter'], () => {
635
+ if (!this.activeModal && !this.activeFileViewer) {
636
+ this.selectCurrentItem();
637
+ }
638
+ });
639
+
640
+ // Escape to close modals
641
+ this.screen.key(['escape'], () => {
642
+ if (this.activeFileViewer) {
643
+ this.closeFileViewer();
644
+ } else if (this.activeModal) {
645
+ this.closeModal();
646
+ }
647
+ });
648
+
649
+ // Hotkeys for actions (work globally, not just in actions pane)
650
+ // Uses the key property from action items for consistent mapping
651
+ const hotkeys = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '-', '='];
652
+ hotkeys.forEach((key) => {
653
+ this.screen.key([key], () => {
654
+ if (!this.activeModal && !this.activeFileViewer) {
655
+ // Find the action item with this key
656
+ const matchingItem = this.actionItems.find(
657
+ (item) => item.key === key && item.type === 'action'
658
+ );
659
+ if (matchingItem) {
660
+ this.handleAction(matchingItem.id);
661
+ }
662
+ }
663
+ });
664
+ });
665
+
666
+ // Toggle hotkeys (O for lOop, P for Parallel)
667
+ this.screen.key(['o'], () => {
668
+ if (!this.activeModal) {
669
+ this.handleAction('toggle-loop');
670
+ }
671
+ });
672
+ this.screen.key(['p'], () => {
673
+ if (!this.activeModal) {
674
+ this.handleAction('toggle-parallel');
675
+ }
676
+ });
677
+
678
+ // Q for quit, R for refresh, L for view logs, C for clear logs
679
+ this.screen.key(['q'], () => {
680
+ if (!this.activeModal) {
681
+ this.handleAction('quit');
682
+ }
683
+ });
684
+ this.screen.key(['r'], () => {
685
+ if (!this.activeModal) {
686
+ this.handleAction('refresh');
687
+ }
688
+ });
689
+ this.screen.key(['v'], () => {
690
+ if (!this.activeModal) {
691
+ this.handleAction('view-logs');
692
+ }
693
+ });
694
+ this.screen.key(['c'], () => {
695
+ if (!this.activeModal) {
696
+ this.handleAction('clear-logs');
697
+ }
698
+ });
699
+
700
+ // Delete agent when 'x' is pressed and status pane is focused with agent selected
701
+ this.screen.key(['x'], () => {
702
+ if (!this.activeModal && this.focusedPane === 'status') {
703
+ this.deleteSelectedAgent();
704
+ }
705
+ });
706
+
707
+ }
708
+
709
+ private cyclePane(direction: number): void {
710
+ const currentIndex = this.paneOrder.indexOf(this.focusedPane);
711
+ const newIndex = (currentIndex + direction + this.paneOrder.length) % this.paneOrder.length;
712
+ this.focusedPane = this.paneOrder[newIndex];
713
+ this.render();
714
+ }
715
+
716
+ private navigatePane(delta: number): void {
717
+ const maxIndex = this.getMaxIndexForPane(this.focusedPane);
718
+ if (maxIndex < 0) return;
719
+
720
+ const currentIndex = this.selectedIndex[this.focusedPane];
721
+ let newIndex = currentIndex + delta;
722
+
723
+ // Clamp to valid range
724
+ newIndex = Math.max(0, Math.min(maxIndex, newIndex));
725
+ this.selectedIndex[this.focusedPane] = newIndex;
726
+
727
+ this.render();
728
+ }
729
+
730
+ private getMaxIndexForPane(pane: PaneId): number {
731
+ switch (pane) {
732
+ case 'actions':
733
+ return this.getSelectableActionItems().length - 1;
734
+ case 'prompts':
735
+ return Math.max(0, this.state.prompts.length - 1);
736
+ case 'status':
737
+ // Status pane has docs + agents as selectable items
738
+ const fileStates = this.getFileStates();
739
+ const selectableItems = getSelectableItems(
740
+ this.state.spec,
741
+ fileStates,
742
+ this.state.activeAgents
743
+ );
744
+ return Math.max(0, selectableItems.length - 1);
745
+ }
746
+ }
747
+
748
+ private selectCurrentItem(): void {
749
+ if (this.focusedPane === 'actions') {
750
+ const selectableItems = this.getSelectableActionItems();
751
+ const item = selectableItems[this.selectedIndex.actions];
752
+ if (item) {
753
+ this.handleAction(item.id);
754
+ }
755
+ } else if (this.focusedPane === 'prompts') {
756
+ const sortedPrompts = this.getSortedPrompts();
757
+ const prompt = sortedPrompts[this.selectedIndex.prompts];
758
+ if (prompt && prompt.path) {
759
+ // Open the prompt file in the file viewer
760
+ const title = `Prompt ${String(prompt.number).padStart(2, '0')}: ${prompt.title}`;
761
+ this.openFileViewer(title, prompt.path);
762
+ }
763
+ } else if (this.focusedPane === 'status') {
764
+ // Status pane: select docs or agents
765
+ const fileStates = this.getFileStates();
766
+ const selectableItems = getSelectableItems(
767
+ this.state.spec,
768
+ fileStates,
769
+ this.state.activeAgents
770
+ );
771
+ const item = selectableItems[this.selectedIndex.status];
772
+ if (item) {
773
+ switch (item.type) {
774
+ case 'spec':
775
+ if (this.state.spec && this.options.cwd) {
776
+ // Try branch first (status.yaml is in branch-based folder), then spec name
777
+ const specPath = getSpecFilePath(this.options.cwd, this.state.branch || this.state.spec);
778
+ if (specPath) {
779
+ this.openFileViewer(`Spec: ${this.state.spec}`, specPath);
780
+ }
781
+ }
782
+ break;
783
+ case 'alignment':
784
+ if (this.state.branch && this.options.cwd) {
785
+ const alignPath = getPlanningFilePath(this.options.cwd, this.state.branch, 'alignment');
786
+ if (alignPath) {
787
+ this.openFileViewer('Alignment Document', alignPath);
788
+ }
789
+ }
790
+ break;
791
+ case 'e2e':
792
+ if (this.state.branch && this.options.cwd) {
793
+ const e2ePath = getPlanningFilePath(this.options.cwd, this.state.branch, 'e2e_test_plan');
794
+ if (e2ePath) {
795
+ this.openFileViewer('E2E Test Plan', e2ePath);
796
+ }
797
+ }
798
+ break;
799
+ case 'agent':
800
+ // For agents, Enter could show details - for now just log
801
+ this.log(`Selected agent: ${item.agentName}`);
802
+ break;
803
+ }
804
+ }
805
+ }
806
+ }
807
+
808
+ /**
809
+ * Delete the currently selected agent (when status pane is focused)
810
+ */
811
+ private deleteSelectedAgent(): void {
812
+ if (this.focusedPane !== 'status') return;
813
+
814
+ const fileStates = this.getFileStates();
815
+ const selectableItems = getSelectableItems(
816
+ this.state.spec,
817
+ fileStates,
818
+ this.state.activeAgents
819
+ );
820
+ const item = selectableItems[this.selectedIndex.status];
821
+
822
+ if (item?.type === 'agent' && item.agentName) {
823
+ const agentName = item.agentName;
824
+ // Find and kill the agent window
825
+ const currentSession = getCurrentSession();
826
+ if (currentSession) {
827
+ try {
828
+ killWindow(currentSession, agentName);
829
+ // Remove from active agents
830
+ this.state.activeAgents = this.state.activeAgents.filter(
831
+ (a) => a.name !== agentName
832
+ );
833
+ this.log(`Deleted agent: ${agentName}`);
834
+ // Adjust selection if needed
835
+ const newMax = this.getMaxIndexForPane('status');
836
+ if (this.selectedIndex.status > newMax) {
837
+ this.selectedIndex.status = Math.max(0, newMax);
838
+ }
839
+ this.render();
840
+ } catch (e) {
841
+ this.log(`Failed to delete agent: ${agentName}`);
842
+ }
843
+ }
844
+ }
845
+ }
846
+
847
+ /**
848
+ * Get prompts sorted the same way they appear in the prompts pane.
849
+ * Order: in_progress first, then pending, then done (each sorted by number).
850
+ */
851
+ private getSortedPrompts(): PromptItem[] {
852
+ const inProgress = this.state.prompts
853
+ .filter((p) => p.status === 'in_progress')
854
+ .sort((a, b) => a.number - b.number);
855
+
856
+ const pending = this.state.prompts
857
+ .filter((p) => p.status === 'pending')
858
+ .sort((a, b) => a.number - b.number);
859
+
860
+ const done = this.state.prompts
861
+ .filter((p) => p.status === 'done')
862
+ .sort((a, b) => a.number - b.number);
863
+
864
+ return [...inProgress, ...pending, ...done];
865
+ }
866
+
867
+ private handleAction(actionId: string): void {
868
+ switch (actionId) {
869
+ case 'quit':
870
+ this.destroy(); // Kill spawned agents and cleanup first
871
+ this.options.onExit();
872
+ break;
873
+ case 'refresh':
874
+ this.render();
875
+ break;
876
+ case 'toggle-loop':
877
+ this.state.loopEnabled = !this.state.loopEnabled;
878
+ this.buildActionItems();
879
+ if (this.eventLoop) {
880
+ this.eventLoop.setLoopEnabled(this.state.loopEnabled);
881
+ }
882
+ this.options.onAction('toggle-loop', { enabled: this.state.loopEnabled });
883
+ this.render();
884
+ break;
885
+ case 'toggle-parallel':
886
+ this.state.parallelEnabled = !this.state.parallelEnabled;
887
+ this.buildActionItems();
888
+ if (this.eventLoop) {
889
+ this.eventLoop.setParallelEnabled(this.state.parallelEnabled);
890
+ // Force tick when enabling to spawn immediately
891
+ if (this.state.parallelEnabled) {
892
+ this.eventLoop.forceTick();
893
+ }
894
+ }
895
+ this.options.onAction('toggle-parallel', { enabled: this.state.parallelEnabled });
896
+ this.render();
897
+ break;
898
+ case 'view-logs':
899
+ this.openLogModal();
900
+ break;
901
+ case 'clear-logs':
902
+ this.clearAllLogs();
903
+ break;
904
+ case 'switch-spec':
905
+ this.openSpecModal();
906
+ break;
907
+ case 'custom-flow':
908
+ this.openCustomFlowModal();
909
+ break;
910
+ case 'pr-action':
911
+ if (this.state.prActionState === 'create-pr') {
912
+ this.options.onAction('create-pr');
913
+ } else if (this.state.prActionState === 'rerun-pr-review') {
914
+ this.options.onAction('rerun-pr-review');
915
+ }
916
+ break;
917
+ case 'review-pr':
918
+ this.options.onAction('review-pr');
919
+ break;
920
+ case 'new-initiative':
921
+ this.openNewInitiativeModal();
922
+ break;
923
+ case 'initiative-steering':
924
+ this.openSteeringDomainModal();
925
+ break;
926
+ default:
927
+ this.options.onAction(actionId);
928
+ }
929
+ }
930
+
931
+ /**
932
+ * Open a modal for selecting the spec type for a new initiative.
933
+ * Routes to the appropriate scoping flow based on selection.
934
+ */
935
+ private openNewInitiativeModal(): void {
936
+ this.activeModal = createModal(this.screen, {
937
+ title: 'New Initiative — Select Type',
938
+ items: WORKFLOW_DOMAIN_ITEMS.map((t) => ({
939
+ id: t.id,
940
+ label: t.label,
941
+ type: 'item' as const,
942
+ })),
943
+ onSelect: (specType: string) => {
944
+ this.closeModal();
945
+ this.options.onAction('new-initiative', { specType });
946
+ },
947
+ onCancel: () => {
948
+ this.closeModal();
949
+ },
950
+ });
951
+ this.screen.render();
952
+ }
953
+
954
+ /**
955
+ * Open a modal for selecting the workflow domain when steering an initiative.
956
+ * Pre-selects the spec's initial_workflow_domain as the default.
957
+ */
958
+ private openSteeringDomainModal(): void {
959
+ // Read spec's initial_workflow_domain for pre-selection
960
+ let defaultDomain = 'milestone';
961
+ const branch = this.state.branch;
962
+ if (branch) {
963
+ const spec = getSpecForBranch(branch, this.options.cwd);
964
+ if (spec) {
965
+ defaultDomain = getWorkflowDomain(spec.path);
966
+ }
967
+ }
968
+
969
+ // Reorder so the default domain appears first (pre-selected)
970
+ const sorted = [
971
+ ...WORKFLOW_DOMAIN_ITEMS.filter((d) => d.id === defaultDomain),
972
+ ...WORKFLOW_DOMAIN_ITEMS.filter((d) => d.id !== defaultDomain),
973
+ ];
974
+
975
+ this.activeModal = createModal(this.screen, {
976
+ title: `Steer Initiative — Select Domain (default: ${defaultDomain})`,
977
+ items: sorted.map((d) => ({
978
+ id: d.id,
979
+ label: d.label,
980
+ type: 'item' as const,
981
+ })),
982
+ onSelect: (domain: string) => {
983
+ this.closeModal();
984
+ this.options.onAction('initiative-steering', { domain });
985
+ },
986
+ onCancel: () => {
987
+ this.closeModal();
988
+ },
989
+ });
990
+ this.screen.render();
991
+ }
992
+
993
+ private openSpecModal(): void {
994
+ // Load specs dynamically from filesystem
995
+ const specGroups = loadAllSpecs(this.options.cwd);
996
+ const items = specsToModalItems(specGroups);
997
+
998
+ this.activeModal = createModal(this.screen, {
999
+ title: this.state.spec ? `Select Spec (current: ${this.state.spec})` : 'Select Spec',
1000
+ items,
1001
+ onSelect: (id: string) => {
1002
+ this.closeModal();
1003
+ this.options.onAction('switch-spec', { specId: id });
1004
+ },
1005
+ onCancel: () => {
1006
+ this.closeModal();
1007
+ },
1008
+ });
1009
+ this.screen.render();
1010
+ }
1011
+
1012
+ private openLogModal(): void {
1013
+ // Reverse logs so newest entries appear at the top
1014
+ const reversedLogs = [...this.logEntries].reverse();
1015
+ this.activeModal = createModal(this.screen, {
1016
+ title: 'Activity Log',
1017
+ items: reversedLogs.map((entry, i) => ({
1018
+ id: `log-${i}`,
1019
+ label: entry,
1020
+ type: 'item' as const,
1021
+ })),
1022
+ onSelect: () => {}, // Log items not selectable
1023
+ onCancel: () => {
1024
+ this.closeModal();
1025
+ },
1026
+ scrollable: true,
1027
+ });
1028
+ this.screen.render();
1029
+ }
1030
+
1031
+ private openCustomFlowModal(): void {
1032
+ // Load flows from filesystem
1033
+ const flowGroups = loadAllFlows();
1034
+ const items = flowsToModalItems(flowGroups);
1035
+
1036
+ this.activeModal = createModal(this.screen, {
1037
+ title: 'Select Flow',
1038
+ items,
1039
+ onSelect: (flowPath: string) => {
1040
+ this.closeModal();
1041
+ // flowPath is the absolute path to the selected flow file
1042
+ if (!flowPath.startsWith('header-')) {
1043
+ this.openCustomMessageInput(flowPath);
1044
+ }
1045
+ },
1046
+ onCancel: () => {
1047
+ this.closeModal();
1048
+ },
1049
+ scrollable: true,
1050
+ });
1051
+ this.screen.render();
1052
+ }
1053
+
1054
+ private openCustomMessageInput(flowPath: string): void {
1055
+ // Create an input modal for the custom message
1056
+ const width = 60;
1057
+ const height = 12;
1058
+
1059
+ const box = blessed.box({
1060
+ parent: this.screen,
1061
+ top: 'center',
1062
+ left: 'center',
1063
+ width,
1064
+ height,
1065
+ border: {
1066
+ type: 'line',
1067
+ },
1068
+ label: ' Custom Message (optional) ',
1069
+ tags: true,
1070
+ style: {
1071
+ border: {
1072
+ fg: '#a78bfa',
1073
+ },
1074
+ },
1075
+ });
1076
+
1077
+ // Add description text
1078
+ blessed.text({
1079
+ parent: box,
1080
+ top: 1,
1081
+ left: 1,
1082
+ content: '{#c7d2fe-fg}Enter a custom message (system prompt).\nLeave empty to skip. Press Enter to confirm.{/#c7d2fe-fg}',
1083
+ tags: true,
1084
+ });
1085
+
1086
+ // Create textarea for input
1087
+ const textarea = blessed.textarea({
1088
+ parent: box,
1089
+ top: 4,
1090
+ left: 1,
1091
+ width: width - 4,
1092
+ height: 4,
1093
+ border: {
1094
+ type: 'line',
1095
+ },
1096
+ style: {
1097
+ border: {
1098
+ fg: '#4A34C5',
1099
+ },
1100
+ focus: {
1101
+ border: {
1102
+ fg: '#a78bfa',
1103
+ },
1104
+ },
1105
+ },
1106
+ inputOnFocus: true,
1107
+ });
1108
+
1109
+ // Help text
1110
+ blessed.text({
1111
+ parent: box,
1112
+ bottom: 0,
1113
+ left: 1,
1114
+ content: '{#5c6370-fg}[Enter] Confirm [Esc] Cancel{/#5c6370-fg}',
1115
+ tags: true,
1116
+ });
1117
+
1118
+ // Store modal reference for cleanup (conform to Modal interface)
1119
+ const modalRef: Modal = {
1120
+ box,
1121
+ selectedIndex: 0,
1122
+ destroy: () => {
1123
+ box.destroy();
1124
+ },
1125
+ navigate: () => {}, // Not used for input modal
1126
+ select: () => {}, // Not used for input modal
1127
+ };
1128
+ this.activeModal = modalRef;
1129
+
1130
+ // Focus textarea
1131
+ textarea.focus();
1132
+
1133
+ // Handle Enter key - submit
1134
+ textarea.key(['enter'], () => {
1135
+ const customMessage = textarea.getValue().trim();
1136
+ textarea.cancel(); // Exit input mode before destroying
1137
+ modalRef.destroy();
1138
+ this.activeModal = null;
1139
+ this.screen.focusPop(); // Restore focus to screen
1140
+ this.spawnCustomFlowAgent(flowPath, customMessage);
1141
+ this.render();
1142
+ });
1143
+
1144
+ // Handle Escape key - cancel
1145
+ textarea.key(['escape'], () => {
1146
+ textarea.cancel(); // Exit input mode before destroying
1147
+ modalRef.destroy();
1148
+ this.activeModal = null;
1149
+ this.screen.focusPop(); // Restore focus to screen
1150
+ this.render();
1151
+ });
1152
+
1153
+ this.screen.render();
1154
+ }
1155
+
1156
+ private spawnCustomFlowAgent(flowPath: string, customMessage: string): void {
1157
+ // Increment counter and generate window name
1158
+ this.state.customFlowCounter++;
1159
+ const windowName = `custom-flow-${this.state.customFlowCounter}`;
1160
+ const branch = this.state.branch || 'main';
1161
+
1162
+ this.log(`Spawning custom flow: ${windowName}`);
1163
+ this.log(`Flow: ${flowPath.split('/').slice(-2).join('/')}`);
1164
+
1165
+ try {
1166
+ const result = spawnCustomFlow(
1167
+ {
1168
+ flowPath,
1169
+ customMessage,
1170
+ windowName,
1171
+ focusWindow: true,
1172
+ specName: this.state.spec,
1173
+ },
1174
+ branch,
1175
+ this.options.cwd
1176
+ );
1177
+
1178
+ this.log(`Spawned ${windowName} in ${result.sessionName}:${result.windowName}`);
1179
+
1180
+ // Update running agents display
1181
+ this.state.activeAgents = [
1182
+ ...this.state.activeAgents,
1183
+ {
1184
+ name: windowName,
1185
+ agentType: 'custom-flow',
1186
+ isRunning: true,
1187
+ },
1188
+ ];
1189
+ this.render();
1190
+ } catch (e) {
1191
+ const message = e instanceof Error ? e.message : String(e);
1192
+ this.log(`Error spawning custom flow: ${message}`);
1193
+ logTuiError('spawnCustomFlow', e instanceof Error ? e : message, {
1194
+ flowPath,
1195
+ windowName,
1196
+ customMessage: customMessage || undefined,
1197
+ spec: this.state.spec,
1198
+ branch: this.state.branch,
1199
+ }, this.options.cwd);
1200
+ }
1201
+ }
1202
+
1203
+ private closeModal(): void {
1204
+ if (this.activeModal) {
1205
+ this.activeModal.destroy();
1206
+ this.activeModal = null;
1207
+ this.render();
1208
+ }
1209
+ }
1210
+
1211
+ /**
1212
+ * Show a confirmation dialog and wait for user response.
1213
+ * Returns true if user confirms, false if cancelled.
1214
+ * Optional detail text renders below the message in a dimmer style.
1215
+ */
1216
+ public showConfirmation(title: string, message: string, detail?: string): Promise<boolean> {
1217
+ return new Promise((resolve) => {
1218
+ if (this.activeModal) {
1219
+ this.closeModal();
1220
+ }
1221
+
1222
+ const width = 60;
1223
+ const messageLines = message.split('\n');
1224
+ // Detail adds a blank separator line plus its own lines
1225
+ const detailLines = detail ? detail.split('\n') : [];
1226
+ const totalContentLines = messageLines.length + (detail ? 1 + detailLines.length : 0);
1227
+ const height = Math.min(totalContentLines + 6, 20);
1228
+
1229
+ const box = blessed.box({
1230
+ parent: this.screen,
1231
+ top: 'center',
1232
+ left: 'center',
1233
+ width,
1234
+ height,
1235
+ border: {
1236
+ type: 'line',
1237
+ },
1238
+ label: ` ${title} `,
1239
+ tags: true,
1240
+ style: {
1241
+ border: {
1242
+ fg: '#a78bfa',
1243
+ },
1244
+ },
1245
+ });
1246
+
1247
+ // Add message text
1248
+ blessed.text({
1249
+ parent: box,
1250
+ top: 1,
1251
+ left: 2,
1252
+ right: 2,
1253
+ content: `{#c7d2fe-fg}${message}{/#c7d2fe-fg}`,
1254
+ tags: true,
1255
+ });
1256
+
1257
+ // Add detail text below message if provided
1258
+ if (detail) {
1259
+ blessed.text({
1260
+ parent: box,
1261
+ top: 1 + messageLines.length + 1, // message offset + message lines + blank separator
1262
+ left: 2,
1263
+ right: 2,
1264
+ content: `{#5c6370-fg}${detail}{/#5c6370-fg}`,
1265
+ tags: true,
1266
+ });
1267
+ }
1268
+
1269
+ // Add button hints
1270
+ blessed.text({
1271
+ parent: box,
1272
+ bottom: 0,
1273
+ left: 1,
1274
+ content: '{#10b981-fg}[Enter]{/#10b981-fg} Proceed {#ef4444-fg}[Esc]{/#ef4444-fg} Cancel',
1275
+ tags: true,
1276
+ });
1277
+
1278
+ // Focus the box for key events
1279
+ box.focus();
1280
+
1281
+ // Store modal reference
1282
+ const modalRef: Modal = {
1283
+ box,
1284
+ selectedIndex: 0,
1285
+ destroy: () => box.destroy(),
1286
+ navigate: () => {},
1287
+ select: () => {},
1288
+ };
1289
+ this.activeModal = modalRef;
1290
+
1291
+ // Handle Enter - confirm
1292
+ box.key(['enter'], () => {
1293
+ modalRef.destroy();
1294
+ this.activeModal = null;
1295
+ this.screen.focusPop();
1296
+ this.render();
1297
+ resolve(true);
1298
+ });
1299
+
1300
+ // Handle Escape - cancel
1301
+ box.key(['escape'], () => {
1302
+ modalRef.destroy();
1303
+ this.activeModal = null;
1304
+ this.screen.focusPop();
1305
+ this.render();
1306
+ resolve(false);
1307
+ });
1308
+
1309
+ this.screen.render();
1310
+ });
1311
+ }
1312
+
1313
+ public openFileViewer(title: string, filePath: string): void {
1314
+ if (this.activeFileViewer) {
1315
+ this.closeFileViewer();
1316
+ }
1317
+ if (this.activeModal) {
1318
+ this.closeModal();
1319
+ }
1320
+
1321
+ this.activeFileViewer = createFileViewer(this.screen, {
1322
+ title,
1323
+ filePath,
1324
+ onClose: () => {
1325
+ this.closeFileViewer();
1326
+ },
1327
+ });
1328
+
1329
+ if (!this.activeFileViewer) {
1330
+ this.log(`File not found: ${filePath}`);
1331
+ }
1332
+ }
1333
+
1334
+ private closeFileViewer(): void {
1335
+ if (this.activeFileViewer) {
1336
+ this.activeFileViewer.destroy();
1337
+ this.activeFileViewer = null;
1338
+ this.render();
1339
+ }
1340
+ }
1341
+
1342
+ /**
1343
+ * Get planning file paths for current spec
1344
+ */
1345
+ public getFileStates(): { spec: boolean; alignment: boolean; e2eTestPlan: boolean } {
1346
+ if (!this.state.branch || !this.options.cwd) {
1347
+ return { spec: false, alignment: false, e2eTestPlan: false };
1348
+ }
1349
+
1350
+ return {
1351
+ spec: this.state.spec ? getSpecFilePath(this.options.cwd, this.state.spec) !== null : false,
1352
+ alignment: getPlanningFilePath(this.options.cwd, this.state.branch, 'alignment') !== null,
1353
+ e2eTestPlan: getPlanningFilePath(this.options.cwd, this.state.branch, 'e2e_test_plan') !== null,
1354
+ };
1355
+ }
1356
+
1357
+ public updateState(updates: Partial<TUIState>): void {
1358
+ this.state = { ...this.state, ...updates };
1359
+
1360
+ // Sync toggle states to event loop if they were updated
1361
+ if ('parallelEnabled' in updates && this.eventLoop) {
1362
+ this.eventLoop.setParallelEnabled(this.state.parallelEnabled);
1363
+ }
1364
+
1365
+ this.buildActionItems();
1366
+ this.render();
1367
+ }
1368
+
1369
+ public getState(): TUIState {
1370
+ return { ...this.state };
1371
+ }
1372
+
1373
+ public log(message: string): void {
1374
+ const timestamp = new Date().toLocaleTimeString();
1375
+ this.logEntries.push(`[${timestamp}] ${message}`);
1376
+ // Keep last 100 entries
1377
+ while (this.logEntries.length > 100) {
1378
+ this.logEntries.shift();
1379
+ }
1380
+ }
1381
+
1382
+ /**
1383
+ * Clear all logs: both trace store (SQLite + JSONL) and in-memory TUI logs
1384
+ */
1385
+ private clearAllLogs(): void {
1386
+ // Clear trace store logs
1387
+ clearLogs(this.options.cwd);
1388
+
1389
+ // Clear in-memory TUI logs
1390
+ this.logEntries = [];
1391
+
1392
+ this.log('Logs cleared');
1393
+ this.render();
1394
+ }
1395
+
1396
+ /**
1397
+ * Set PR URL for PR review feedback monitoring
1398
+ */
1399
+ public setPRUrl(url: string | null): void {
1400
+ if (this.eventLoop) {
1401
+ this.eventLoop.setPRUrl(url);
1402
+ }
1403
+ if (url) {
1404
+ this.state.prActionState = 'awaiting-review';
1405
+ this.buildActionItems();
1406
+ this.render();
1407
+ }
1408
+ }
1409
+
1410
+ /**
1411
+ * Sync EventLoop's branch context after TUI-initiated branch changes.
1412
+ *
1413
+ * Call this after switch-spec, clear-spec, or mark-completed to prevent
1414
+ * the EventLoop from detecting a "stale" branch change and overwriting
1415
+ * the TUI's correct state with incorrect data from findSpecByBranch().
1416
+ *
1417
+ * @param branch - The new branch name
1418
+ * @param spec - The spec for this branch (or null if no spec)
1419
+ */
1420
+ public syncBranchContext(branch: string, spec: SpecFile | null): void {
1421
+ if (this.eventLoop) {
1422
+ this.eventLoop.setBranchContext(branch, spec);
1423
+ }
1424
+ }
1425
+
1426
+ private render(): void {
1427
+ // Update actions pane
1428
+ this.actionsPane.destroy();
1429
+ this.actionsPane = createActionsPane(
1430
+ this.screen,
1431
+ this.getToggleState(),
1432
+ this.focusedPane === 'actions' ? this.selectedIndex.actions : undefined
1433
+ );
1434
+
1435
+ // Update prompts pane
1436
+ this.promptsPane.destroy();
1437
+ this.promptsPane = createPromptsPane(
1438
+ this.screen,
1439
+ this.state.prompts,
1440
+ this.focusedPane === 'prompts' ? this.selectedIndex.prompts : undefined
1441
+ );
1442
+
1443
+ // Update status pane
1444
+ this.statusPane.destroy();
1445
+ const fileStates = this.getFileStates();
1446
+ this.statusPane = createStatusPane(
1447
+ this.screen,
1448
+ this.state.activeAgents,
1449
+ this.focusedPane === 'status' ? this.selectedIndex.status : undefined,
1450
+ this.state.spec,
1451
+ this.state.branch,
1452
+ this.state.baseBranch,
1453
+ this.logEntries,
1454
+ fileStates,
1455
+ {
1456
+ onViewSpec: () => {
1457
+ if (this.state.spec && this.options.cwd) {
1458
+ // Try branch first (status.yaml is in branch-based folder), then spec name
1459
+ const specPath = getSpecFilePath(this.options.cwd, this.state.branch || this.state.spec);
1460
+ if (specPath) {
1461
+ this.openFileViewer(`Spec: ${this.state.spec}`, specPath);
1462
+ }
1463
+ }
1464
+ },
1465
+ onViewAlignment: () => {
1466
+ if (this.state.branch && this.options.cwd) {
1467
+ const alignPath = getPlanningFilePath(this.options.cwd, this.state.branch, 'alignment');
1468
+ if (alignPath) {
1469
+ this.openFileViewer('Alignment Document', alignPath);
1470
+ }
1471
+ }
1472
+ },
1473
+ onViewE2ETestPlan: () => {
1474
+ if (this.state.branch && this.options.cwd) {
1475
+ const e2ePath = getPlanningFilePath(this.options.cwd, this.state.branch, 'e2e_test_plan');
1476
+ if (e2ePath) {
1477
+ this.openFileViewer('E2E Test Plan', e2ePath);
1478
+ }
1479
+ }
1480
+ },
1481
+ }
1482
+ );
1483
+
1484
+ // Apply focus styling
1485
+ this.applyFocusStyles();
1486
+
1487
+ this.screen.render();
1488
+ }
1489
+
1490
+ private applyFocusStyles(): void {
1491
+ // Highlight focused pane border
1492
+ const panes: Record<PaneId, blessed.Widgets.BoxElement> = {
1493
+ actions: this.actionsPane,
1494
+ prompts: this.promptsPane,
1495
+ status: this.statusPane,
1496
+ };
1497
+
1498
+ for (const [paneId, pane] of Object.entries(panes)) {
1499
+ const isFocused = paneId === this.focusedPane;
1500
+ if (pane.style && pane.style.border) {
1501
+ // Focused: bright purple, Unfocused: muted purple
1502
+ pane.style.border.fg = isFocused ? '#a78bfa' : '#4A34C5';
1503
+ }
1504
+ }
1505
+ }
1506
+
1507
+ public destroy(): void {
1508
+ // Stop CLI daemon
1509
+ if (this.cliDaemon) {
1510
+ this.cliDaemon.stop();
1511
+ }
1512
+
1513
+ // Stop event loop daemon
1514
+ if (this.eventLoop) {
1515
+ this.eventLoop.stop();
1516
+ }
1517
+
1518
+ // Only kill windows that were spawned by this TUI session
1519
+ const currentSession = getCurrentSession();
1520
+ if (currentSession) {
1521
+ const hubWindowId = getHubWindowId(this.options.cwd);
1522
+ const spawnedWindows = getSpawnedWindows(this.options.cwd);
1523
+ const windows = listWindows(currentSession);
1524
+
1525
+ for (const window of windows) {
1526
+ // Skip the hub window - check by ID (stable) or name (fallback)
1527
+ if (window.id === hubWindowId || window.name === 'hub') continue;
1528
+
1529
+ // Only kill windows that were spawned by this TUI session
1530
+ if (!spawnedWindows.includes(window.name)) continue;
1531
+
1532
+ try {
1533
+ killWindow(currentSession, window.name);
1534
+ } catch (e) {
1535
+ // Log but don't fail - window might already be closed
1536
+ logTuiError('killWindow', e instanceof Error ? e : String(e), {
1537
+ session: currentSession,
1538
+ window: window.name,
1539
+ }, this.options.cwd);
1540
+ }
1541
+ }
1542
+ }
1543
+
1544
+ // Clear the TUI session state
1545
+ clearTuiSession(this.options.cwd);
1546
+
1547
+ try {
1548
+ this.screen.destroy();
1549
+ } finally {
1550
+ // Restore original output functions after cleanup
1551
+ // Delay restoration to catch any deferred terminal output
1552
+ const savedStdout = this.originalStdoutWrite;
1553
+ const savedStderr = this.originalStderrWrite;
1554
+ const savedConsoleLog = this.originalConsoleLog;
1555
+ const savedConsoleError = this.originalConsoleError;
1556
+
1557
+ setTimeout(() => {
1558
+ if (savedStdout) process.stdout.write = savedStdout;
1559
+ if (savedStderr) process.stderr.write = savedStderr;
1560
+ if (savedConsoleLog) console.log = savedConsoleLog;
1561
+ if (savedConsoleError) console.error = savedConsoleError;
1562
+ }, 100);
1563
+ }
1564
+ }
1565
+
1566
+ public start(): void {
1567
+ this.screen.render();
1568
+ }
1569
+ }
1570
+
1571
+ export type { ActionItem } from './actions.js';
1572
+ export type { PromptItem } from './prompts-pane.js';
1573
+ export type { AgentInfo } from './status-pane.js';
1574
+