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,662 @@
1
+ /**
2
+ * Event Loop Daemon
3
+ *
4
+ * Non-blocking event loop that monitors external state:
5
+ * 1. PR review feedback polling (configurable reviewer)
6
+ * 2. Git branch change detection (and associated spec changes)
7
+ * 3. Agent window status monitoring
8
+ * 4. Prompt execution loop (when enabled)
9
+ *
10
+ * In the branch-keyed model:
11
+ * - The current git branch determines the active spec
12
+ * - Branch changes trigger spec context updates
13
+ * - No separate "active spec" tracking needed
14
+ */
15
+
16
+ import { getCurrentBranch, updatePRReviewStatus, sanitizeBranchForDir, readStatus } from './planning.js';
17
+ import { loadProjectSettings } from '../hooks/shared.js';
18
+ import { listWindows, SESSION_NAME, sessionExists, getCurrentSession, getSpawnedAgentRegistry, unregisterSpawnedAgent } from './tmux.js';
19
+ import { pickNextPrompt, markPromptInProgress, loadAllPrompts, type PromptFile } from './prompts.js';
20
+ import { shutdownDaemon } from './mcp-client.js';
21
+ import { getSpecForBranch, type SpecFile } from './specs.js';
22
+ import {
23
+ checkPRReviewStatus,
24
+ hasNewReview,
25
+ parsePRUrl,
26
+ type PRReviewState,
27
+ } from './pr-review.js';
28
+
29
+ /** Cooldown between spawns to prevent race conditions with tmux registry */
30
+ const SPAWN_COOLDOWN_MS = 10000;
31
+
32
+ export interface PromptSnapshot {
33
+ count: number;
34
+ pending: number;
35
+ inProgress: number;
36
+ done: number;
37
+ /** Hash of prompt filenames + statuses for change detection */
38
+ hash: string;
39
+ }
40
+
41
+ export interface EventLoopState {
42
+ currentBranch: string;
43
+ currentSpec: SpecFile | null;
44
+ planningKey: string | null; // Sanitized branch name for .planning/ lookup
45
+ prUrl: string | null;
46
+ prReviewFeedbackAvailable: boolean;
47
+ prReviewState: PRReviewState;
48
+ activeAgents: string[];
49
+ lastCheckTime: number;
50
+ loopEnabled: boolean;
51
+ parallelEnabled: boolean; // Parallel execution mode
52
+ /** Active executor prompt numbers (supports parallel execution) */
53
+ activeExecutorPrompts: number[];
54
+ /** Timestamp of last executor spawn (for race condition protection) */
55
+ lastExecutorSpawnTime: number | null;
56
+ /** Last known prompt state for change detection */
57
+ promptSnapshot: PromptSnapshot | null;
58
+ /** Tick counter for modulus-based polling */
59
+ tickCount: number;
60
+ /** Consecutive emergent planner spawns without new prompt creation (resets when new pending prompts appear) */
61
+ emergentSpawnCount: number;
62
+ /** Total prompt count at last emergent planner spawn, used to detect whether it produced new prompts */
63
+ emergentLastPromptCount: number;
64
+ }
65
+
66
+ export interface EventLoopCallbacks {
67
+ onPRReviewFeedback?: (available: boolean) => void;
68
+ onBranchChange?: (newBranch: string, spec: SpecFile | null) => void;
69
+ onAgentsChange?: (agents: string[]) => void;
70
+ onSpawnExecutor?: (prompt: PromptFile) => void;
71
+ /** Called when emergent planner should be spawned (no pending + no in_progress) */
72
+ onSpawnEmergentPlanning?: () => void;
73
+ onLoopStatus?: (message: string) => void;
74
+ /** Called when prompts are added, removed, or their status changes */
75
+ onPromptsChange?: (prompts: PromptFile[], snapshot: PromptSnapshot) => void;
76
+ }
77
+
78
+ // Note: Use isLockedBranch() from planning.ts for consistent branch checks
79
+
80
+ export class EventLoop {
81
+ private intervalId: NodeJS.Timeout | null = null;
82
+ private pollIntervalMs: number;
83
+ private state: EventLoopState;
84
+ private callbacks: EventLoopCallbacks;
85
+ private cwd: string;
86
+
87
+ // PR review settings from .allhands/settings.json
88
+ private prReviewCheckFrequency: number;
89
+ private reviewMatchPattern: string;
90
+ private rerunComment: string;
91
+
92
+ constructor(
93
+ cwd: string,
94
+ callbacks: EventLoopCallbacks = {},
95
+ pollIntervalMs?: number
96
+ ) {
97
+ this.cwd = cwd;
98
+ this.callbacks = callbacks;
99
+
100
+ // Load settings from .allhands/settings.json
101
+ const settings = loadProjectSettings();
102
+ this.pollIntervalMs = pollIntervalMs ?? settings?.eventLoop?.tickIntervalMs ?? 5000;
103
+ this.prReviewCheckFrequency = settings?.prReview?.checkFrequency ?? 3;
104
+ this.reviewMatchPattern = settings?.prReview?.reviewMatchPattern ?? 'greptile';
105
+ this.rerunComment = settings?.prReview?.rerunComment ?? '@greptile';
106
+
107
+ // Initialize state based on current branch
108
+ const currentBranch = getCurrentBranch(cwd);
109
+ const currentSpec = getSpecForBranch(currentBranch, cwd);
110
+ const planningKey = sanitizeBranchForDir(currentBranch);
111
+
112
+ this.state = {
113
+ currentBranch,
114
+ currentSpec,
115
+ planningKey,
116
+ prUrl: null,
117
+ prReviewFeedbackAvailable: false,
118
+ prReviewState: {
119
+ status: 'none',
120
+ lastCommentId: null,
121
+ lastCommentTime: null,
122
+ reviewCycle: 0,
123
+ },
124
+ activeAgents: [],
125
+ lastCheckTime: Date.now(),
126
+ loopEnabled: false,
127
+ parallelEnabled: false,
128
+ activeExecutorPrompts: [],
129
+ lastExecutorSpawnTime: null,
130
+ promptSnapshot: null,
131
+ tickCount: 0,
132
+ emergentSpawnCount: 0,
133
+ emergentLastPromptCount: 0,
134
+ };
135
+ }
136
+
137
+ /**
138
+ * Start the event loop
139
+ */
140
+ start(): void {
141
+ if (this.intervalId) {
142
+ return; // Already running
143
+ }
144
+
145
+ this.intervalId = setInterval(() => {
146
+ this.tick().catch((err) => {
147
+ console.error('[EventLoop] Error in tick:', err);
148
+ });
149
+ }, this.pollIntervalMs);
150
+
151
+ // Run initial tick
152
+ this.tick().catch((err) => {
153
+ console.error('[EventLoop] Error in initial tick:', err);
154
+ });
155
+ }
156
+
157
+ /**
158
+ * Stop the event loop and clean up
159
+ */
160
+ stop(): void {
161
+ if (this.intervalId) {
162
+ clearInterval(this.intervalId);
163
+ this.intervalId = null;
164
+ }
165
+ }
166
+
167
+ /**
168
+ * Set the PR URL to monitor for PR review feedback
169
+ */
170
+ setPRUrl(url: string | null): void {
171
+ this.state.prUrl = url;
172
+ this.state.prReviewFeedbackAvailable = false;
173
+ }
174
+
175
+ /**
176
+ * Enable or disable the prompt execution loop
177
+ */
178
+ setLoopEnabled(enabled: boolean): void {
179
+ this.state.loopEnabled = enabled;
180
+ this.state.emergentSpawnCount = 0;
181
+ if (!enabled) {
182
+ this.state.activeExecutorPrompts = [];
183
+ this.state.lastExecutorSpawnTime = null;
184
+ }
185
+ }
186
+
187
+ /**
188
+ * Enable or disable parallel execution mode
189
+ */
190
+ setParallelEnabled(enabled: boolean): void {
191
+ this.state.parallelEnabled = enabled;
192
+ }
193
+
194
+ /**
195
+ * Force an immediate tick of the event loop.
196
+ * Use when enabling parallel mode to spawn immediately without waiting.
197
+ */
198
+ async forceTick(): Promise<void> {
199
+ await this.tick();
200
+ }
201
+
202
+ /**
203
+ * Get current state
204
+ */
205
+ getState(): EventLoopState {
206
+ return { ...this.state };
207
+ }
208
+
209
+ /**
210
+ * Manually set branch context after TUI-initiated branch changes.
211
+ *
212
+ * This prevents the EventLoop from triggering onBranchChange callbacks
213
+ * for changes that the TUI already handled (e.g., switch-spec, clear-spec).
214
+ * Without this, the EventLoop would detect the branch change on its next
215
+ * tick and potentially overwrite TUI state with stale/incorrect data.
216
+ */
217
+ setBranchContext(branch: string, spec: SpecFile | null): void {
218
+ this.state.currentBranch = branch;
219
+ this.state.currentSpec = spec;
220
+ this.state.planningKey = sanitizeBranchForDir(branch);
221
+ }
222
+
223
+ /**
224
+ * Main tick - runs all checks
225
+ */
226
+ private async tick(): Promise<void> {
227
+ this.state.lastCheckTime = Date.now();
228
+ this.state.tickCount++;
229
+
230
+ await Promise.all([
231
+ this.checkPRReviewFeedback(),
232
+ this.checkGitBranch(),
233
+ this.checkAgentWindows(),
234
+ this.checkPromptFiles(),
235
+ ]);
236
+
237
+ // Check prompt loop after agent windows (needs to know active agents)
238
+ await this.checkPromptLoop();
239
+ }
240
+
241
+ /**
242
+ * Check for PR review feedback
243
+ *
244
+ * Uses the pr-review library to:
245
+ * - Track review cycles (not just presence)
246
+ * - Compare comment timestamps with last check
247
+ * - Update status.yaml with current state
248
+ *
249
+ * Polls at a configurable frequency (every N ticks) since reviews
250
+ * can take several minutes to complete.
251
+ */
252
+ private async checkPRReviewFeedback(): Promise<void> {
253
+ if (!this.state.prUrl) {
254
+ return;
255
+ }
256
+
257
+ // Only poll every prReviewCheckFrequency ticks (not every tick)
258
+ if (this.state.tickCount % this.prReviewCheckFrequency !== 0) {
259
+ return;
260
+ }
261
+
262
+ // Validate PR URL
263
+ const prInfo = parsePRUrl(this.state.prUrl);
264
+ if (!prInfo) {
265
+ return;
266
+ }
267
+
268
+ try {
269
+ // Get lastReviewRunTime from status.yaml to filter comments
270
+ let afterTime: string | undefined;
271
+ if (this.state.planningKey) {
272
+ try {
273
+ const status = readStatus(this.state.planningKey, this.cwd);
274
+ afterTime = status?.prReview?.lastReviewRunTime ?? undefined;
275
+ } catch {
276
+ // Status file might not exist - ignore
277
+ }
278
+ }
279
+
280
+ // Get current PR review state
281
+ const currentState = await checkPRReviewStatus(
282
+ this.state.prUrl,
283
+ this.reviewMatchPattern,
284
+ afterTime,
285
+ this.cwd
286
+ );
287
+
288
+ // Check if there's a new review
289
+ const isNewReview = hasNewReview(this.state.prReviewState, currentState);
290
+
291
+ // Update feedback available flag
292
+ const hasReviewComment = currentState.status === 'completed';
293
+
294
+ if (hasReviewComment !== this.state.prReviewFeedbackAvailable || isNewReview) {
295
+ this.state.prReviewFeedbackAvailable = hasReviewComment;
296
+ this.state.prReviewState = currentState;
297
+
298
+ // Update status.yaml with PR review state (use planning key)
299
+ if (this.state.planningKey) {
300
+ try {
301
+ updatePRReviewStatus(
302
+ {
303
+ reviewCycle: currentState.reviewCycle,
304
+ lastReviewTime: currentState.lastCommentTime,
305
+ status: currentState.status,
306
+ },
307
+ this.state.planningKey,
308
+ this.cwd
309
+ );
310
+ } catch {
311
+ // Status file might not exist - ignore
312
+ }
313
+ }
314
+
315
+ // Notify if there's a new review
316
+ if (isNewReview) {
317
+ this.callbacks.onPRReviewFeedback?.(hasReviewComment);
318
+ }
319
+ }
320
+ } catch {
321
+ // Silently fail - might not have gh installed or no PR
322
+ }
323
+ }
324
+
325
+ /**
326
+ * Check for git branch changes
327
+ *
328
+ * In the branch-keyed model:
329
+ * - Branch changes are the primary trigger for context changes
330
+ * - Find the spec for the new branch via findSpecByBranch()
331
+ * - Notify callbacks so TUI can update state
332
+ */
333
+ private async checkGitBranch(): Promise<void> {
334
+ try {
335
+ const currentBranch = getCurrentBranch(this.cwd);
336
+
337
+ if (currentBranch !== this.state.currentBranch) {
338
+ // Branch changed - update spec context
339
+ this.state.currentBranch = currentBranch;
340
+ this.state.currentSpec = getSpecForBranch(currentBranch, this.cwd);
341
+ this.state.planningKey = sanitizeBranchForDir(currentBranch);
342
+
343
+ // Notify callback with branch and spec info
344
+ this.callbacks.onBranchChange?.(currentBranch, this.state.currentSpec);
345
+ }
346
+ } catch (err) {
347
+ console.error('[EventLoop] checkGitBranch failed:', err);
348
+ }
349
+ }
350
+
351
+ /**
352
+ * Check for changes in prompt files
353
+ *
354
+ * The harness is the coordinator and watcher - it detects when agents
355
+ * create, modify, or delete prompt files and notifies the TUI.
356
+ * This enables reactive UI updates without agents needing to know about the harness.
357
+ */
358
+ private async checkPromptFiles(): Promise<void> {
359
+ // Need planning directory to check prompts
360
+ if (!this.state.planningKey) {
361
+ return;
362
+ }
363
+
364
+ try {
365
+ const prompts = loadAllPrompts(this.state.planningKey, this.cwd);
366
+ const snapshot = this.computePromptSnapshot(prompts);
367
+
368
+ // Check if prompts have changed
369
+ const hasChanged = !this.state.promptSnapshot ||
370
+ snapshot.hash !== this.state.promptSnapshot.hash;
371
+
372
+ if (hasChanged) {
373
+ // Reset emergent planner backoff when new pending prompts appear (external prompt creation)
374
+ if (this.state.promptSnapshot && snapshot.pending > this.state.promptSnapshot.pending) {
375
+ this.state.emergentSpawnCount = 0;
376
+ }
377
+ this.state.promptSnapshot = snapshot;
378
+ this.callbacks.onPromptsChange?.(prompts, snapshot);
379
+ }
380
+ } catch (err) {
381
+ console.error('[EventLoop] checkPromptFiles failed:', err);
382
+ }
383
+ }
384
+
385
+ /**
386
+ * Compute a snapshot of prompt state for change detection
387
+ */
388
+ private computePromptSnapshot(prompts: PromptFile[]): PromptSnapshot {
389
+ let pending = 0;
390
+ let inProgress = 0;
391
+ let done = 0;
392
+
393
+ // Build hash from filenames + statuses + numbers
394
+ const parts: string[] = [];
395
+ for (const p of prompts) {
396
+ parts.push(`${p.filename}:${p.frontmatter.status}:${p.frontmatter.number}`);
397
+ switch (p.frontmatter.status) {
398
+ case 'pending': pending++; break;
399
+ case 'in_progress': inProgress++; break;
400
+ case 'done': done++; break;
401
+ }
402
+ }
403
+ parts.sort(); // Consistent ordering
404
+
405
+ return {
406
+ count: prompts.length,
407
+ pending,
408
+ inProgress,
409
+ done,
410
+ hash: parts.join('|'),
411
+ };
412
+ }
413
+
414
+ /**
415
+ * Check for changes in agent windows
416
+ *
417
+ * Only tracks agents that were spawned by ALL HANDS (in the registry)
418
+ * and still exist in tmux. This prevents picking up unrelated tmux windows.
419
+ */
420
+ private async checkAgentWindows(): Promise<void> {
421
+ try {
422
+ // Use current session if in tmux, otherwise fall back to SESSION_NAME
423
+ const currentSession = getCurrentSession();
424
+ const sessionName = currentSession || SESSION_NAME;
425
+
426
+ if (!sessionExists(sessionName)) {
427
+ // Session gone - cleanup all agent daemons
428
+ if (this.state.activeAgents.length > 0) {
429
+ await this.cleanupAgentDaemons(this.state.activeAgents);
430
+ // Also clear the registry since session is gone
431
+ for (const name of this.state.activeAgents) {
432
+ unregisterSpawnedAgent(name);
433
+ }
434
+ this.state.activeAgents = [];
435
+ this.callbacks.onAgentsChange?.([]);
436
+ }
437
+ return;
438
+ }
439
+
440
+ const windows = listWindows(sessionName);
441
+ const registry = getSpawnedAgentRegistry();
442
+
443
+ // Only include windows that:
444
+ // 1. Are in our spawned registry (were created by ALL HANDS)
445
+ // 2. Still exist in tmux
446
+ // 3. Are not the TUI/hub window
447
+ const agentWindows = windows
448
+ .filter((w) => w.index > 0 && w.name !== 'hub' && registry.has(w.name))
449
+ .map((w) => w.name);
450
+
451
+ // Check if agents have changed
452
+ const sortedCurrent = [...agentWindows].sort();
453
+ const sortedPrevious = [...this.state.activeAgents].sort();
454
+
455
+ if (JSON.stringify(sortedCurrent) !== JSON.stringify(sortedPrevious)) {
456
+ // Find agents that disappeared and cleanup their daemons
457
+ const disappeared = this.state.activeAgents.filter(
458
+ (name) => !agentWindows.includes(name)
459
+ );
460
+
461
+ if (disappeared.length > 0) {
462
+ await this.cleanupAgentDaemons(disappeared);
463
+ // Unregister disappeared agents
464
+ for (const name of disappeared) {
465
+ unregisterSpawnedAgent(name);
466
+ }
467
+
468
+ // If an executor exited, remove it from activeExecutorPrompts
469
+ // Extract prompt number from window name (e.g., "executor-03" -> 3)
470
+ for (const name of disappeared) {
471
+ if (name.startsWith('executor')) {
472
+ const match = name.match(/-(\d+)$/);
473
+ if (match) {
474
+ const promptNum = parseInt(match[1], 10);
475
+ this.state.activeExecutorPrompts = this.state.activeExecutorPrompts.filter(
476
+ (n) => n !== promptNum
477
+ );
478
+ }
479
+ }
480
+ }
481
+ // Clear spawn timestamp to allow new spawns
482
+ if (disappeared.some((name) => name.startsWith('executor') || name.startsWith('emergent'))) {
483
+ this.state.lastExecutorSpawnTime = null;
484
+ }
485
+ }
486
+
487
+ this.state.activeAgents = agentWindows;
488
+ this.callbacks.onAgentsChange?.(agentWindows);
489
+ }
490
+
491
+ // Reconcile activeExecutorPrompts with actual running agents
492
+ // This handles cases where an agent dies before being detected in activeAgents
493
+ // (e.g., immediate compaction after spawn)
494
+ if (this.state.activeExecutorPrompts.length > 0) {
495
+ const runningPromptNums = new Set<number>();
496
+ for (const name of agentWindows) {
497
+ if (name.startsWith('executor')) {
498
+ const match = name.match(/-(\d+)$/);
499
+ if (match) {
500
+ runningPromptNums.add(parseInt(match[1], 10));
501
+ }
502
+ }
503
+ }
504
+ // Keep only prompts that have a running agent
505
+ const before = this.state.activeExecutorPrompts.length;
506
+ this.state.activeExecutorPrompts = this.state.activeExecutorPrompts.filter(
507
+ (n) => runningPromptNums.has(n)
508
+ );
509
+ // If we cleaned up stale prompts, also clear spawn timestamp
510
+ if (this.state.activeExecutorPrompts.length < before) {
511
+ this.state.lastExecutorSpawnTime = null;
512
+ }
513
+ }
514
+ } catch (err) {
515
+ console.error('[EventLoop] checkAgentWindows failed:', err);
516
+ }
517
+ }
518
+
519
+ /**
520
+ * Cleanup MCP daemons for agents that have exited.
521
+ * The window name IS the AGENT_ID, so we can directly shutdown their daemons.
522
+ */
523
+ private async cleanupAgentDaemons(agentNames: string[]): Promise<void> {
524
+ for (const agentName of agentNames) {
525
+ try {
526
+ // Window name = AGENT_ID for daemon isolation
527
+ await shutdownDaemon(agentName);
528
+ } catch {
529
+ // Ignore errors - daemon may already be gone
530
+ }
531
+ }
532
+ }
533
+
534
+ /**
535
+ * Unified prompt loop — single decision path:
536
+ *
537
+ * 1. loop enabled + pending prompts → pick next, spawn executor
538
+ * 2. loop enabled + no pending + no in_progress → spawn emergent planner
539
+ * 3. loop enabled + no pending + in_progress exist → wait (executors still working)
540
+ * 4. loop disabled → nothing
541
+ *
542
+ * Parallel rules: spawn up to maxParallel executors; only ONE emergent planner at a time.
543
+ */
544
+ private async checkPromptLoop(): Promise<void> {
545
+ if (!this.state.loopEnabled) {
546
+ return;
547
+ }
548
+
549
+ // Need a planning directory to pick prompts (spec file is optional metadata)
550
+ if (!this.state.planningKey) {
551
+ this.callbacks.onLoopStatus?.('No planning directory for this branch - loop paused');
552
+ return;
553
+ }
554
+
555
+ try {
556
+ // Execution gating: only spawn executors/emergent planners when stage is 'executing'
557
+ try {
558
+ const status = readStatus(this.state.planningKey, this.cwd);
559
+ if (status?.stage !== 'executing') {
560
+ this.callbacks.onLoopStatus?.(`Stage is '${status?.stage || 'unknown'}' — waiting for executing stage`);
561
+ return;
562
+ }
563
+ } catch {
564
+ // No status file — cannot determine stage, skip spawning
565
+ return;
566
+ }
567
+
568
+ // Block if emergent planner is running (only ONE at a time)
569
+ const hasEmergent = this.state.activeAgents.some((name) => name.startsWith('emergent'));
570
+ if (hasEmergent) {
571
+ return;
572
+ }
573
+
574
+ // Count active executors
575
+ const activeExecutors = this.state.activeAgents.filter((name) => name.startsWith('executor'));
576
+
577
+ // Determine max parallel based on toggle
578
+ const settings = loadProjectSettings();
579
+ const maxParallel = this.state.parallelEnabled
580
+ ? (settings?.spawn?.maxParallelPrompts ?? 3)
581
+ : 1;
582
+
583
+ // Check capacity - if at max, don't spawn
584
+ if (activeExecutors.length >= maxParallel) {
585
+ return;
586
+ }
587
+
588
+ // Time-based guard: don't spawn if we spawned recently (within cooldown)
589
+ if (
590
+ this.state.lastExecutorSpawnTime &&
591
+ Date.now() - this.state.lastExecutorSpawnTime < SPAWN_COOLDOWN_MS
592
+ ) {
593
+ return;
594
+ }
595
+
596
+ // Pick next prompt from planning directory
597
+ const result = pickNextPrompt(
598
+ this.state.planningKey,
599
+ this.cwd,
600
+ this.state.activeExecutorPrompts
601
+ );
602
+
603
+ if (result.prompt) {
604
+ // Pending prompt available → spawn executor
605
+ markPromptInProgress(result.prompt.path);
606
+ this.state.activeExecutorPrompts.push(result.prompt.frontmatter.number);
607
+ this.state.lastExecutorSpawnTime = Date.now();
608
+
609
+ this.callbacks.onLoopStatus?.(
610
+ `Spawning executor for prompt ${result.prompt.frontmatter.number}: ${result.prompt.frontmatter.title}`
611
+ );
612
+ this.callbacks.onSpawnExecutor?.(result.prompt);
613
+ return;
614
+ }
615
+
616
+ // No pending prompts — check if we should spawn emergent planner
617
+ if (
618
+ result.stats &&
619
+ result.stats.pending === 0 &&
620
+ result.stats.inProgress === 0
621
+ ) {
622
+ // Track whether prior emergent planner spawn was productive (created new prompts)
623
+ const currentPromptCount = this.state.promptSnapshot?.count ?? 0;
624
+ if (currentPromptCount <= this.state.emergentLastPromptCount) {
625
+ // Emergent planner spawned but didn't produce new prompts — unproductive
626
+ this.state.emergentSpawnCount++;
627
+ } else {
628
+ // Emergent planner produced work — reset backoff
629
+ this.state.emergentSpawnCount = 0;
630
+ }
631
+
632
+ // Apply exponential backoff for unproductive spawns
633
+ const cooldownMs = SPAWN_COOLDOWN_MS * Math.pow(2, Math.min(this.state.emergentSpawnCount, 4));
634
+ if (
635
+ this.state.lastExecutorSpawnTime &&
636
+ Date.now() - this.state.lastExecutorSpawnTime < cooldownMs
637
+ ) {
638
+ this.callbacks.onLoopStatus?.(
639
+ `Emergent planner backoff: waiting ${cooldownMs / 1000}s (${this.state.emergentSpawnCount} unproductive spawns)`
640
+ );
641
+ return;
642
+ }
643
+
644
+ // Spawn emergent planner
645
+ this.state.lastExecutorSpawnTime = Date.now();
646
+ this.state.emergentLastPromptCount = currentPromptCount;
647
+
648
+ this.callbacks.onLoopStatus?.(
649
+ `Spawning emergent planner (attempt ${this.state.emergentSpawnCount + 1})`
650
+ );
651
+ this.callbacks.onSpawnEmergentPlanning?.();
652
+ return;
653
+ }
654
+
655
+ // In-progress prompts still running — wait
656
+ this.callbacks.onLoopStatus?.(result.reason);
657
+ } catch (err) {
658
+ console.error('[EventLoop] checkPromptLoop failed:', err);
659
+ }
660
+ }
661
+
662
+ }