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.
- package/.allhands/README.md +75 -0
- package/.allhands/agents/compounder.yaml +15 -0
- package/.allhands/agents/coordinator.yaml +17 -0
- package/.allhands/agents/documentor.yaml +15 -0
- package/.allhands/agents/e2e-test-planner.yaml +17 -0
- package/.allhands/agents/emergent.yaml +22 -0
- package/.allhands/agents/executor.yaml +14 -0
- package/.allhands/agents/ideation.yaml +11 -0
- package/.allhands/agents/initiative-steering.yaml +19 -0
- package/.allhands/agents/judge.yaml +13 -0
- package/.allhands/agents/planner.yaml +19 -0
- package/.allhands/agents/pr-reviewer.yaml +15 -0
- package/.allhands/docs.json +5 -0
- package/.allhands/docs.local.json +26 -0
- package/.allhands/flows/COMPOUNDING.md +203 -0
- package/.allhands/flows/COORDINATION.md +89 -0
- package/.allhands/flows/CORE.md +87 -0
- package/.allhands/flows/DOCUMENTATION.md +218 -0
- package/.allhands/flows/E2E_TEST_PLAN_BUILDING.md +140 -0
- package/.allhands/flows/EMERGENT_PLANNING.md +57 -0
- package/.allhands/flows/IDEATION_SCOPING.md +154 -0
- package/.allhands/flows/INITIATIVE_STEERING.md +110 -0
- package/.allhands/flows/JUDGE_REVIEWING.md +79 -0
- package/.allhands/flows/PROMPT_TASK_EXECUTION.md +68 -0
- package/.allhands/flows/PR_REVIEWING.md +43 -0
- package/.allhands/flows/SPEC_PLANNING.md +216 -0
- package/.allhands/flows/harness/WRITING_HARNESS_FLOWS.md +27 -0
- package/.allhands/flows/harness/WRITING_HARNESS_KNOWLEDGE.md +27 -0
- package/.allhands/flows/harness/WRITING_HARNESS_ORCHESTRATION.md +27 -0
- package/.allhands/flows/harness/WRITING_HARNESS_SKILLS.md +27 -0
- package/.allhands/flows/harness/WRITING_HARNESS_TOOLS.md +27 -0
- package/.allhands/flows/harness/WRITING_HARNESS_VALIDATION_TOOLING.md +27 -0
- package/.allhands/flows/shared/CODEBASE_UNDERSTANDING.md +72 -0
- package/.allhands/flows/shared/CREATE_HARNESS_SPEC.md +48 -0
- package/.allhands/flows/shared/CREATE_SPEC.md +41 -0
- package/.allhands/flows/shared/CREATE_VALIDATION_TOOLING_SPEC.md +70 -0
- package/.allhands/flows/shared/DOCUMENTATION_DISCOVERY.md +123 -0
- package/.allhands/flows/shared/DOCUMENTATION_WRITER.md +101 -0
- package/.allhands/flows/shared/EMERGENT_REFINEMENT_ANALYSIS.md +76 -0
- package/.allhands/flows/shared/EXTERNAL_TECH_GUIDANCE.md +97 -0
- package/.allhands/flows/shared/IDEATION_CODEBASE_GROUNDING.md +49 -0
- package/.allhands/flows/shared/PLAN_DEEPENING.md +152 -0
- package/.allhands/flows/shared/PROMPT_TASKS_CURATION.md +113 -0
- package/.allhands/flows/shared/PROMPT_VALIDATION_REVIEW.MD +99 -0
- package/.allhands/flows/shared/QUICK_PREMORTEM.md +70 -0
- package/.allhands/flows/shared/RESEARCH_GUIDANCE.md +38 -0
- package/.allhands/flows/shared/REVIEW_OPTIONS_BREAKDOWN.md +68 -0
- package/.allhands/flows/shared/SKILL_EXTRACTION.md +84 -0
- package/.allhands/flows/shared/SPEC_FLOW_ANALYSIS.md +119 -0
- package/.allhands/flows/shared/TDD_WORKFLOW.md +109 -0
- package/.allhands/flows/shared/UTILIZE_VALIDATION_TOOLING.md +84 -0
- package/.allhands/flows/shared/WRITING_HARNESS_FLOWS.md +11 -0
- package/.allhands/flows/shared/WRITING_HARNESS_MCP_TOOLS.md +84 -0
- package/.allhands/flows/shared/jury/ARCHITECTURE_REVIEW.md +91 -0
- package/.allhands/flows/shared/jury/BEST_PRACTICES_REVIEW.md +80 -0
- package/.allhands/flows/shared/jury/CLAIM_VERIFICATION_REVIEW.md +101 -0
- package/.allhands/flows/shared/jury/EXPECTATIONS_FIT_REVIEW.md +78 -0
- package/.allhands/flows/shared/jury/MAINTAINABILITY_REVIEW.md +110 -0
- package/.allhands/flows/shared/jury/PROMPTS_EXPECTATIONS_FIT.md +74 -0
- package/.allhands/flows/shared/jury/PROMPTS_FLOW_ANALYSIS.md +92 -0
- package/.allhands/flows/shared/jury/PROMPTS_YAGNI.md +78 -0
- package/.allhands/flows/shared/jury/PROMPT_PREMORTEM.md +125 -0
- package/.allhands/flows/shared/jury/SECURITY_REVIEW.md +86 -0
- package/.allhands/flows/shared/jury/YAGNI_REVIEW.md +82 -0
- package/.allhands/flows/wip/DEBUG_INVESTIGATION.md +162 -0
- package/.allhands/flows/wip/MEMORY_RECALL.md +62 -0
- package/.allhands/harness/ah +131 -0
- package/.allhands/harness/package-lock.json +5292 -0
- package/.allhands/harness/package.json +52 -0
- package/.allhands/harness/src/__tests__/e2e/commands.test.ts +307 -0
- package/.allhands/harness/src/__tests__/e2e/event-loop.test.ts +539 -0
- package/.allhands/harness/src/__tests__/e2e/hooks.test.ts +427 -0
- package/.allhands/harness/src/__tests__/e2e/new-initiative-routing.test.ts +137 -0
- package/.allhands/harness/src/__tests__/e2e/run-e2e.ts +109 -0
- package/.allhands/harness/src/__tests__/e2e/specs-type.test.ts +210 -0
- package/.allhands/harness/src/__tests__/e2e/validation-hooks.test.ts +669 -0
- package/.allhands/harness/src/__tests__/e2e/validation-path-consistency.test.ts +354 -0
- package/.allhands/harness/src/__tests__/e2e/validation.test.ts +528 -0
- package/.allhands/harness/src/__tests__/harness/assertions.ts +318 -0
- package/.allhands/harness/src/__tests__/harness/cli-runner.ts +359 -0
- package/.allhands/harness/src/__tests__/harness/fixture.ts +384 -0
- package/.allhands/harness/src/__tests__/harness/hook-runner.ts +411 -0
- package/.allhands/harness/src/__tests__/harness/index.ts +122 -0
- package/.allhands/harness/src/cli.ts +36 -0
- package/.allhands/harness/src/commands/complexity.ts +177 -0
- package/.allhands/harness/src/commands/context7.ts +202 -0
- package/.allhands/harness/src/commands/docs.ts +557 -0
- package/.allhands/harness/src/commands/hooks.ts +24 -0
- package/.allhands/harness/src/commands/index.ts +51 -0
- package/.allhands/harness/src/commands/knowledge.ts +382 -0
- package/.allhands/harness/src/commands/memories.ts +302 -0
- package/.allhands/harness/src/commands/notify.ts +61 -0
- package/.allhands/harness/src/commands/oracle.ts +158 -0
- package/.allhands/harness/src/commands/perplexity.ts +220 -0
- package/.allhands/harness/src/commands/planning.ts +245 -0
- package/.allhands/harness/src/commands/schema.ts +73 -0
- package/.allhands/harness/src/commands/skills.ts +128 -0
- package/.allhands/harness/src/commands/solutions.ts +353 -0
- package/.allhands/harness/src/commands/spawn.ts +158 -0
- package/.allhands/harness/src/commands/specs.ts +532 -0
- package/.allhands/harness/src/commands/tavily.ts +226 -0
- package/.allhands/harness/src/commands/tools.ts +579 -0
- package/.allhands/harness/src/commands/trace.ts +327 -0
- package/.allhands/harness/src/commands/tui.ts +960 -0
- package/.allhands/harness/src/commands/validate.ts +143 -0
- package/.allhands/harness/src/commands/validation-tools.ts +108 -0
- package/.allhands/harness/src/hooks/context.ts +1442 -0
- package/.allhands/harness/src/hooks/enforcement.ts +170 -0
- package/.allhands/harness/src/hooks/index.ts +54 -0
- package/.allhands/harness/src/hooks/lifecycle.ts +229 -0
- package/.allhands/harness/src/hooks/notification.ts +104 -0
- package/.allhands/harness/src/hooks/observability.ts +551 -0
- package/.allhands/harness/src/hooks/session.ts +88 -0
- package/.allhands/harness/src/hooks/shared.ts +815 -0
- package/.allhands/harness/src/hooks/transcript-parser.ts +208 -0
- package/.allhands/harness/src/hooks/validation.ts +617 -0
- package/.allhands/harness/src/lib/__tests__/ctags.test.ts +244 -0
- package/.allhands/harness/src/lib/__tests__/docs-validation.test.ts +344 -0
- package/.allhands/harness/src/lib/__tests__/mcp-runtime.test.ts +190 -0
- package/.allhands/harness/src/lib/__tests__/schema.test.ts +861 -0
- package/.allhands/harness/src/lib/base-command.ts +198 -0
- package/.allhands/harness/src/lib/cli-daemon.ts +343 -0
- package/.allhands/harness/src/lib/compaction.ts +313 -0
- package/.allhands/harness/src/lib/ctags.ts +497 -0
- package/.allhands/harness/src/lib/docs-validation.ts +907 -0
- package/.allhands/harness/src/lib/event-loop.ts +662 -0
- package/.allhands/harness/src/lib/flows.ts +155 -0
- package/.allhands/harness/src/lib/git.ts +276 -0
- package/.allhands/harness/src/lib/knowledge-worker.ts +72 -0
- package/.allhands/harness/src/lib/knowledge.ts +810 -0
- package/.allhands/harness/src/lib/llm.ts +255 -0
- package/.allhands/harness/src/lib/mcp-client.ts +432 -0
- package/.allhands/harness/src/lib/mcp-daemon.ts +486 -0
- package/.allhands/harness/src/lib/mcp-runtime.ts +418 -0
- package/.allhands/harness/src/lib/notification.ts +115 -0
- package/.allhands/harness/src/lib/opencode/index.ts +70 -0
- package/.allhands/harness/src/lib/opencode/profiles.ts +300 -0
- package/.allhands/harness/src/lib/opencode/prompts/codesearch.md +98 -0
- package/.allhands/harness/src/lib/opencode/prompts/knowledge-aggregator.md +67 -0
- package/.allhands/harness/src/lib/opencode/runner.ts +281 -0
- package/.allhands/harness/src/lib/oracle.ts +926 -0
- package/.allhands/harness/src/lib/planning-utils.ts +150 -0
- package/.allhands/harness/src/lib/planning.ts +605 -0
- package/.allhands/harness/src/lib/pr-review.ts +225 -0
- package/.allhands/harness/src/lib/prompts.ts +522 -0
- package/.allhands/harness/src/lib/schema.ts +418 -0
- package/.allhands/harness/src/lib/schemas/agent-profile.ts +141 -0
- package/.allhands/harness/src/lib/schemas/template-vars.ts +138 -0
- package/.allhands/harness/src/lib/session.ts +164 -0
- package/.allhands/harness/src/lib/specs.ts +348 -0
- package/.allhands/harness/src/lib/tldr.ts +829 -0
- package/.allhands/harness/src/lib/tmux.ts +1051 -0
- package/.allhands/harness/src/lib/trace-store.ts +714 -0
- package/.allhands/harness/src/mcp/__tests__/index.test.ts +46 -0
- package/.allhands/harness/src/mcp/_template.ts +47 -0
- package/.allhands/harness/src/mcp/filesystem.ts +33 -0
- package/.allhands/harness/src/mcp/index.ts +69 -0
- package/.allhands/harness/src/mcp/playwright.ts +34 -0
- package/.allhands/harness/src/mcp/xcodebuild.ts +29 -0
- package/.allhands/harness/src/schemas/docs.schema.json +44 -0
- package/.allhands/harness/src/schemas/settings.schema.json +214 -0
- package/.allhands/harness/src/tui/actions.ts +227 -0
- package/.allhands/harness/src/tui/file-viewer-modal.ts +270 -0
- package/.allhands/harness/src/tui/index.ts +1574 -0
- package/.allhands/harness/src/tui/modal.ts +232 -0
- package/.allhands/harness/src/tui/prompts-pane.ts +186 -0
- package/.allhands/harness/src/tui/status-pane.ts +434 -0
- package/.allhands/harness/tsconfig.json +22 -0
- package/.allhands/harness/vitest.config.ts +13 -0
- package/.allhands/pillars.md +33 -0
- package/.allhands/principles.md +88 -0
- package/.allhands/schemas/alignment.yaml +51 -0
- package/.allhands/schemas/documentation.yaml +10 -0
- package/.allhands/schemas/prompt.yaml +92 -0
- package/.allhands/schemas/skill.yaml +34 -0
- package/.allhands/schemas/solution.yaml +131 -0
- package/.allhands/schemas/spec.yaml +67 -0
- package/.allhands/schemas/validation-suite.yaml +49 -0
- package/.allhands/schemas/workflow.yaml +51 -0
- package/.allhands/settings.json +57 -0
- package/.allhands/skills/claude-code-patterns/SKILL.md +60 -0
- package/.allhands/skills/claude-code-patterns/docs/context-hygiene.md +19 -0
- package/.allhands/skills/harness-maintenance/SKILL.md +449 -0
- package/.allhands/skills/harness-maintenance/references/core-architecture.md +187 -0
- package/.allhands/skills/harness-maintenance/references/harness-skills.md +87 -0
- package/.allhands/skills/harness-maintenance/references/knowledge-compounding.md +78 -0
- package/.allhands/skills/harness-maintenance/references/tools-commands-mcp-hooks.md +115 -0
- package/.allhands/skills/harness-maintenance/references/validation-tooling.md +77 -0
- package/.allhands/skills/harness-maintenance/references/writing-flows.md +84 -0
- package/.allhands/validation/browser-automation.md +109 -0
- package/.allhands/validation/xcode-automation.md +195 -0
- package/.allhands/workflows/documentation.md +86 -0
- package/.allhands/workflows/investigation.md +81 -0
- package/.allhands/workflows/milestone.md +91 -0
- package/.allhands/workflows/optimization.md +85 -0
- package/.allhands/workflows/refactor.md +99 -0
- package/.allhands/workflows/triage.md +81 -0
- package/.claude/README.md +1 -0
- package/.claude/agents/explorer.md +10 -0
- package/.claude/agents/researcher.md +11 -0
- package/.claude/agents/task-runner.md +8 -0
- package/.claude/settings.json +231 -0
- package/.env.ai.example +7 -0
- package/.github/workflows/npm-publish.yml +69 -0
- package/.internal.json +45 -0
- package/.tldr/config.json +11 -0
- package/.tldrignore +90 -0
- package/CLAUDE.md +6 -0
- package/README.md +98 -0
- package/bin/sync-cli.js +7552 -0
- package/concerns.md +7 -0
- package/docs/README.md +41 -0
- package/docs/agents/README.md +24 -0
- package/docs/agents/agent-configuration-system.md +86 -0
- package/docs/agents/execution-agents.md +50 -0
- package/docs/agents/knowledge-agents.md +61 -0
- package/docs/agents/orchestration-agent.md +57 -0
- package/docs/agents/planning-agents.md +84 -0
- package/docs/agents/quality-review-agents.md +67 -0
- package/docs/agents/workflow-agent-orchestration.md +69 -0
- package/docs/flows/README.md +44 -0
- package/docs/flows/compounding.md +126 -0
- package/docs/flows/coordination.md +72 -0
- package/docs/flows/core-harness-integration.md +63 -0
- package/docs/flows/documentation-orchestration.md +98 -0
- package/docs/flows/e2e-test-plan-building.md +83 -0
- package/docs/flows/emergent-refinement.md +104 -0
- package/docs/flows/flow-authoring-and-mcp-tools.md +89 -0
- package/docs/flows/judge-reviewing.md +112 -0
- package/docs/flows/plan-deepening-and-research.md +107 -0
- package/docs/flows/plan-review-jury.md +114 -0
- package/docs/flows/pr-reviewing.md +54 -0
- package/docs/flows/prompt-task-execution.md +119 -0
- package/docs/flows/spec-planning.md +162 -0
- package/docs/flows/type-specific-scoping-flows.md +49 -0
- package/docs/flows/validation-and-skills-integration.md +145 -0
- package/docs/flows/wip/wip-flows.md +102 -0
- package/docs/harness/README.md +23 -0
- package/docs/harness/agent-profiles.md +84 -0
- package/docs/harness/cli/README.md +24 -0
- package/docs/harness/cli/cli-entry-and-command-discovery.md +91 -0
- package/docs/harness/cli/docs-command.md +87 -0
- package/docs/harness/cli/knowledge-command.md +91 -0
- package/docs/harness/cli/minor-cli-commands.md +65 -0
- package/docs/harness/cli/oracle-command.md +113 -0
- package/docs/harness/cli/planning-command.md +95 -0
- package/docs/harness/cli/schema-and-validation-commands.md +154 -0
- package/docs/harness/cli/search-commands.md +97 -0
- package/docs/harness/cli/spawn-command.md +136 -0
- package/docs/harness/cli/specs-command.md +102 -0
- package/docs/harness/cli/tools-command.md +122 -0
- package/docs/harness/cli/trace-command.md +122 -0
- package/docs/harness/cli-daemon.md +92 -0
- package/docs/harness/event-loop.md +184 -0
- package/docs/harness/hooks/README.md +15 -0
- package/docs/harness/hooks/context-hooks.md +96 -0
- package/docs/harness/hooks/lifecycle-and-observability-hooks.md +135 -0
- package/docs/harness/hooks/validation-hooks.md +97 -0
- package/docs/harness/test-harness.md +149 -0
- package/docs/harness/tui.md +176 -0
- package/docs/memories.md +20 -0
- package/docs/solutions/agentic-issues/premature-agent-deletion-tui-action-dependency-20260130.md +49 -0
- package/docs/solutions/agentic-issues/ref-anchor-scope-mismatch-skill-references-20260131.md +55 -0
- package/docs/solutions/agentic-issues/tautological-tests-routing-20260131.md +52 -0
- package/docs/solutions/integration_issue/blocktool-output-format-mismatch-hook-runner-20260130.md +52 -0
- package/docs/solutions/integration_issue/dual-validation-path-divergence-schema-20260130.md +66 -0
- package/docs/solutions/security-issues/unsanitized-domain-path-join-20260131.md +52 -0
- package/docs/solutions/test-failures/event-loop-mock-ordering-checkAgentWindows-20260130.md +63 -0
- package/docs/sync-cli/README.md +19 -0
- package/docs/sync-cli/cli-entrypoint-and-commands.md +39 -0
- package/docs/sync-cli/commands/README.md +11 -0
- package/docs/sync-cli/commands/pull-manifest-command.md +36 -0
- package/docs/sync-cli/commands/push-command.md +84 -0
- package/docs/sync-cli/commands/sync-command.md +71 -0
- package/docs/sync-cli/systems/README.md +14 -0
- package/docs/sync-cli/systems/git-and-github-integration.md +49 -0
- package/docs/sync-cli/systems/interactive-ui.md +43 -0
- package/docs/sync-cli/systems/manifest-and-distribution.md +51 -0
- package/docs/sync-cli/systems/path-resolution.md +42 -0
- package/package.json +46 -0
- package/scripts/install-shim.sh +40 -0
- package/scripts/pre-pack.sh +25 -0
- package/specs/harness-maintenance-skill.spec.md +138 -0
- package/specs/roadmap/git-spec-lifecycle-management.spec.md +113 -0
- package/specs/sync-init-flag.spec.md +117 -0
- package/specs/unified-workflow-orchestration.spec.md +250 -0
- package/specs/validation-tooling-practice.spec.md +98 -0
- package/specs/workflow-domain-configuration.spec.md +265 -0
- package/src/commands/pull-manifest.ts +31 -0
- package/src/commands/push.ts +344 -0
- package/src/commands/sync.ts +289 -0
- package/src/lib/constants.ts +10 -0
- package/src/lib/dotfiles.ts +36 -0
- package/src/lib/fs-utils.ts +18 -0
- package/src/lib/gh.ts +40 -0
- package/src/lib/git.ts +63 -0
- package/src/lib/gitignore.ts +167 -0
- package/src/lib/manifest.ts +121 -0
- package/src/lib/marker-sync.ts +39 -0
- package/src/lib/paths.ts +38 -0
- package/src/lib/target-lines.ts +66 -0
- package/src/lib/ui.ts +78 -0
- package/src/sync-cli.ts +120 -0
- package/target-lines.json +23 -0
- 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
|
+
}
|