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,539 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Event Loop Decision Logic Tests
|
|
3
|
+
*
|
|
4
|
+
* Exercises the unified checkPromptLoop() decision branches via the public
|
|
5
|
+
* forceTick() method, testing:
|
|
6
|
+
* - 4 unified decision branches (spawn executor, spawn emergent planner, wait, disabled)
|
|
7
|
+
* - Parallel execution capacity enforcement
|
|
8
|
+
* - Spawn cooldown timer (10s SPAWN_COOLDOWN_MS)
|
|
9
|
+
* - Emergent planner singleton blocking
|
|
10
|
+
* - activeExecutorPrompts reconciliation on agent exit
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { describe, it, expect, beforeAll, beforeEach, afterAll, vi } from 'vitest';
|
|
14
|
+
import type { PromptFile, PickerResult } from '../../lib/prompts.js';
|
|
15
|
+
import type { EventLoopCallbacks } from '../../lib/event-loop.js';
|
|
16
|
+
import { createFixture, type TestFixture } from '../harness/index.js';
|
|
17
|
+
|
|
18
|
+
// ─── Mocks (hoisted before imports) ─────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
vi.mock('../../lib/tmux.js', () => ({
|
|
21
|
+
listWindows: vi.fn(() => [{ index: 0, name: 'hub', id: '@0' }]),
|
|
22
|
+
sessionExists: vi.fn(() => true),
|
|
23
|
+
getSpawnedAgentRegistry: vi.fn(() => new Set<string>()),
|
|
24
|
+
getCurrentSession: vi.fn(() => 'test-session'),
|
|
25
|
+
SESSION_NAME: 'all-hands',
|
|
26
|
+
unregisterSpawnedAgent: vi.fn(),
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
vi.mock('../../lib/prompts.js', () => ({
|
|
30
|
+
pickNextPrompt: vi.fn(() => ({
|
|
31
|
+
prompt: null,
|
|
32
|
+
reason: 'No prompt files found',
|
|
33
|
+
stats: { total: 0, pending: 0, inProgress: 0, done: 0, blocked: 0 },
|
|
34
|
+
})),
|
|
35
|
+
loadAllPrompts: vi.fn(() => []),
|
|
36
|
+
markPromptInProgress: vi.fn(),
|
|
37
|
+
}));
|
|
38
|
+
|
|
39
|
+
vi.mock('../../hooks/shared.js', () => ({
|
|
40
|
+
loadProjectSettings: vi.fn(() => ({
|
|
41
|
+
spawn: { maxParallelPrompts: 3 },
|
|
42
|
+
eventLoop: { tickIntervalMs: 1000 },
|
|
43
|
+
})),
|
|
44
|
+
}));
|
|
45
|
+
|
|
46
|
+
vi.mock('../../lib/planning.js', () => ({
|
|
47
|
+
getCurrentBranch: vi.fn(() => 'feature/test-branch'),
|
|
48
|
+
sanitizeBranchForDir: vi.fn(() => 'feature-test-branch'),
|
|
49
|
+
readStatus: vi.fn(() => ({ stage: 'executing' })),
|
|
50
|
+
updatePRReviewStatus: vi.fn(),
|
|
51
|
+
}));
|
|
52
|
+
|
|
53
|
+
vi.mock('../../lib/specs.js', () => ({
|
|
54
|
+
getSpecForBranch: vi.fn(() => null),
|
|
55
|
+
}));
|
|
56
|
+
|
|
57
|
+
vi.mock('../../lib/mcp-client.js', () => ({
|
|
58
|
+
shutdownDaemon: vi.fn(() => Promise.resolve()),
|
|
59
|
+
}));
|
|
60
|
+
|
|
61
|
+
vi.mock('../../lib/pr-review.js', () => ({
|
|
62
|
+
checkPRReviewStatus: vi.fn(() =>
|
|
63
|
+
Promise.resolve({ status: 'none', lastCommentId: null, lastCommentTime: null, reviewCycle: 0 })
|
|
64
|
+
),
|
|
65
|
+
hasNewReview: vi.fn(() => false),
|
|
66
|
+
parsePRUrl: vi.fn(() => null),
|
|
67
|
+
}));
|
|
68
|
+
|
|
69
|
+
// Imports after mocks (vi.mock calls are hoisted)
|
|
70
|
+
import { EventLoop } from '../../lib/event-loop.js';
|
|
71
|
+
import { pickNextPrompt, markPromptInProgress, loadAllPrompts } from '../../lib/prompts.js';
|
|
72
|
+
import { listWindows, getSpawnedAgentRegistry } from '../../lib/tmux.js';
|
|
73
|
+
|
|
74
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
function makePrompt(
|
|
77
|
+
number: number,
|
|
78
|
+
status: 'pending' | 'in_progress' | 'done' = 'pending',
|
|
79
|
+
): PromptFile {
|
|
80
|
+
return {
|
|
81
|
+
path: `/tmp/prompts/${number.toString().padStart(2, '0')}-test.prompt.md`,
|
|
82
|
+
filename: `${number.toString().padStart(2, '0')}-test.prompt.md`,
|
|
83
|
+
frontmatter: {
|
|
84
|
+
number,
|
|
85
|
+
title: `Test Prompt ${number}`,
|
|
86
|
+
status,
|
|
87
|
+
dependencies: [],
|
|
88
|
+
priority: 'medium',
|
|
89
|
+
attempts: 0,
|
|
90
|
+
commits: [],
|
|
91
|
+
created: '2026-01-30T00:00:00.000Z',
|
|
92
|
+
updated: '2026-01-30T00:00:00.000Z',
|
|
93
|
+
},
|
|
94
|
+
body: '## Tasks\n\n- Test task',
|
|
95
|
+
rawContent: '',
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function pickerResult(
|
|
100
|
+
prompt: PromptFile | null,
|
|
101
|
+
stats: PickerResult['stats'],
|
|
102
|
+
reason = '',
|
|
103
|
+
): PickerResult {
|
|
104
|
+
return { prompt, reason, stats };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ─── Tests ───────────────────────────────────────────────────────────────────
|
|
108
|
+
|
|
109
|
+
describe('EventLoop Decision Logic', () => {
|
|
110
|
+
let fixture: TestFixture;
|
|
111
|
+
let loop: EventLoop;
|
|
112
|
+
let callbacks: Required<EventLoopCallbacks>;
|
|
113
|
+
|
|
114
|
+
beforeAll(() => {
|
|
115
|
+
fixture = createFixture({ name: 'event-loop-test' });
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
afterAll(() => {
|
|
119
|
+
fixture.cleanup();
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
beforeEach(() => {
|
|
123
|
+
vi.clearAllMocks();
|
|
124
|
+
|
|
125
|
+
// Reset mocks that individual tests may override
|
|
126
|
+
vi.mocked(listWindows).mockReturnValue([{ index: 0, name: 'hub', id: '@0' }]);
|
|
127
|
+
vi.mocked(getSpawnedAgentRegistry).mockReturnValue(new Set());
|
|
128
|
+
vi.mocked(pickNextPrompt).mockReturnValue({
|
|
129
|
+
prompt: null,
|
|
130
|
+
reason: 'No prompt files found',
|
|
131
|
+
stats: { total: 0, pending: 0, inProgress: 0, done: 0, blocked: 0 },
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
callbacks = {
|
|
135
|
+
onPRReviewFeedback: vi.fn(),
|
|
136
|
+
onBranchChange: vi.fn(),
|
|
137
|
+
onAgentsChange: vi.fn(),
|
|
138
|
+
onSpawnExecutor: vi.fn(),
|
|
139
|
+
onSpawnEmergentPlanning: vi.fn(),
|
|
140
|
+
onLoopStatus: vi.fn(),
|
|
141
|
+
onPromptsChange: vi.fn(),
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
loop = new EventLoop(fixture.root, callbacks);
|
|
145
|
+
loop.setLoopEnabled(true);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// ─── 4 Unified Decision Branches ─────────────────────────────────────
|
|
149
|
+
|
|
150
|
+
describe('unified decision branches', () => {
|
|
151
|
+
it('spawns executor when pending prompt available', async () => {
|
|
152
|
+
const prompt = makePrompt(1);
|
|
153
|
+
vi.mocked(pickNextPrompt).mockReturnValue(
|
|
154
|
+
pickerResult(prompt, { total: 3, pending: 1, inProgress: 1, done: 1, blocked: 0 }),
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
await loop.forceTick();
|
|
158
|
+
|
|
159
|
+
expect(callbacks.onSpawnExecutor).toHaveBeenCalledWith(prompt);
|
|
160
|
+
expect(callbacks.onSpawnEmergentPlanning).not.toHaveBeenCalled();
|
|
161
|
+
expect(markPromptInProgress).toHaveBeenCalledWith(prompt.path);
|
|
162
|
+
expect(loop.getState().activeExecutorPrompts).toContain(1);
|
|
163
|
+
expect(loop.getState().lastExecutorSpawnTime).not.toBeNull();
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('spawns emergent planner when no pending and no in_progress', async () => {
|
|
167
|
+
vi.mocked(pickNextPrompt).mockReturnValue(
|
|
168
|
+
pickerResult(null, { total: 5, pending: 0, inProgress: 0, done: 5, blocked: 0 }),
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
await loop.forceTick();
|
|
172
|
+
|
|
173
|
+
expect(callbacks.onSpawnEmergentPlanning).toHaveBeenCalledOnce();
|
|
174
|
+
expect(callbacks.onSpawnExecutor).not.toHaveBeenCalled();
|
|
175
|
+
expect(loop.getState().lastExecutorSpawnTime).not.toBeNull();
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('waits when no pending but in_progress exist', async () => {
|
|
179
|
+
vi.mocked(pickNextPrompt).mockReturnValue(
|
|
180
|
+
pickerResult(null, { total: 5, pending: 0, inProgress: 2, done: 3, blocked: 0 }, 'Executors still working'),
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
await loop.forceTick();
|
|
184
|
+
|
|
185
|
+
expect(callbacks.onSpawnExecutor).not.toHaveBeenCalled();
|
|
186
|
+
expect(callbacks.onSpawnEmergentPlanning).not.toHaveBeenCalled();
|
|
187
|
+
expect(callbacks.onLoopStatus).toHaveBeenCalledWith('Executors still working');
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('does nothing when loop disabled', async () => {
|
|
191
|
+
loop.setLoopEnabled(false);
|
|
192
|
+
vi.mocked(pickNextPrompt).mockReturnValue(
|
|
193
|
+
pickerResult(makePrompt(1), { total: 3, pending: 1, inProgress: 0, done: 2, blocked: 0 }),
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
await loop.forceTick();
|
|
197
|
+
|
|
198
|
+
expect(callbacks.onSpawnExecutor).not.toHaveBeenCalled();
|
|
199
|
+
expect(callbacks.onSpawnEmergentPlanning).not.toHaveBeenCalled();
|
|
200
|
+
expect(pickNextPrompt).not.toHaveBeenCalled();
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
// ─── Parallel Execution Capacity ──────────────────────────────────────
|
|
205
|
+
|
|
206
|
+
describe('parallel execution capacity', () => {
|
|
207
|
+
it('blocks spawn at max parallel capacity', async () => {
|
|
208
|
+
loop.setParallelEnabled(true);
|
|
209
|
+
|
|
210
|
+
// 3 active executors = maxParallelPrompts (3)
|
|
211
|
+
vi.mocked(listWindows).mockReturnValue([
|
|
212
|
+
{ index: 0, name: 'hub', id: '@0' },
|
|
213
|
+
{ index: 1, name: 'executor-01', id: '@1' },
|
|
214
|
+
{ index: 2, name: 'executor-02', id: '@2' },
|
|
215
|
+
{ index: 3, name: 'executor-03', id: '@3' },
|
|
216
|
+
]);
|
|
217
|
+
vi.mocked(getSpawnedAgentRegistry).mockReturnValue(
|
|
218
|
+
new Set(['executor-01', 'executor-02', 'executor-03']),
|
|
219
|
+
);
|
|
220
|
+
vi.mocked(pickNextPrompt).mockReturnValue(
|
|
221
|
+
pickerResult(makePrompt(4), { total: 6, pending: 1, inProgress: 3, done: 2, blocked: 0 }),
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
await loop.forceTick();
|
|
225
|
+
|
|
226
|
+
expect(callbacks.onSpawnExecutor).not.toHaveBeenCalled();
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it('allows spawn below max parallel capacity', async () => {
|
|
230
|
+
loop.setParallelEnabled(true);
|
|
231
|
+
|
|
232
|
+
// 2 active executors < maxParallelPrompts (3)
|
|
233
|
+
vi.mocked(listWindows).mockReturnValue([
|
|
234
|
+
{ index: 0, name: 'hub', id: '@0' },
|
|
235
|
+
{ index: 1, name: 'executor-01', id: '@1' },
|
|
236
|
+
{ index: 2, name: 'executor-02', id: '@2' },
|
|
237
|
+
]);
|
|
238
|
+
vi.mocked(getSpawnedAgentRegistry).mockReturnValue(
|
|
239
|
+
new Set(['executor-01', 'executor-02']),
|
|
240
|
+
);
|
|
241
|
+
const prompt = makePrompt(3);
|
|
242
|
+
vi.mocked(pickNextPrompt).mockReturnValue(
|
|
243
|
+
pickerResult(prompt, { total: 5, pending: 1, inProgress: 2, done: 2, blocked: 0 }),
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
await loop.forceTick();
|
|
247
|
+
|
|
248
|
+
expect(callbacks.onSpawnExecutor).toHaveBeenCalledWith(prompt);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it('limits to 1 executor when parallel disabled', async () => {
|
|
252
|
+
loop.setParallelEnabled(false);
|
|
253
|
+
|
|
254
|
+
// 1 executor already running — parallel disabled means max=1
|
|
255
|
+
vi.mocked(listWindows).mockReturnValue([
|
|
256
|
+
{ index: 0, name: 'hub', id: '@0' },
|
|
257
|
+
{ index: 1, name: 'executor-01', id: '@1' },
|
|
258
|
+
]);
|
|
259
|
+
vi.mocked(getSpawnedAgentRegistry).mockReturnValue(new Set(['executor-01']));
|
|
260
|
+
vi.mocked(pickNextPrompt).mockReturnValue(
|
|
261
|
+
pickerResult(makePrompt(2), { total: 4, pending: 1, inProgress: 1, done: 2, blocked: 0 }),
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
await loop.forceTick();
|
|
265
|
+
|
|
266
|
+
expect(callbacks.onSpawnExecutor).not.toHaveBeenCalled();
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
// ─── Spawn Cooldown Timer ─────────────────────────────────────────────
|
|
271
|
+
|
|
272
|
+
describe('spawn cooldown timer', () => {
|
|
273
|
+
it('suppresses spawn within 10s SPAWN_COOLDOWN_MS window', async () => {
|
|
274
|
+
// Use parallel so capacity doesn't block before cooldown check
|
|
275
|
+
loop.setParallelEnabled(true);
|
|
276
|
+
|
|
277
|
+
// Tick 1: spawn executor (no executor windows yet)
|
|
278
|
+
vi.mocked(pickNextPrompt).mockReturnValue(
|
|
279
|
+
pickerResult(makePrompt(1), { total: 3, pending: 2, inProgress: 0, done: 1, blocked: 0 }),
|
|
280
|
+
);
|
|
281
|
+
await loop.forceTick();
|
|
282
|
+
expect(callbacks.onSpawnExecutor).toHaveBeenCalledTimes(1);
|
|
283
|
+
|
|
284
|
+
// Tick 2: executor-01 window now visible (prevents reconciliation from
|
|
285
|
+
// clearing the timestamp), but cooldown still active → blocks spawn
|
|
286
|
+
vi.mocked(listWindows).mockReturnValue([
|
|
287
|
+
{ index: 0, name: 'hub', id: '@0' },
|
|
288
|
+
{ index: 1, name: 'executor-01', id: '@1' },
|
|
289
|
+
]);
|
|
290
|
+
vi.mocked(getSpawnedAgentRegistry).mockReturnValue(new Set(['executor-01']));
|
|
291
|
+
vi.mocked(pickNextPrompt).mockReturnValue(
|
|
292
|
+
pickerResult(makePrompt(2), { total: 3, pending: 1, inProgress: 1, done: 1, blocked: 0 }),
|
|
293
|
+
);
|
|
294
|
+
await loop.forceTick();
|
|
295
|
+
expect(callbacks.onSpawnExecutor).toHaveBeenCalledTimes(1); // unchanged
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it('allows spawn after cooldown expires', async () => {
|
|
299
|
+
loop.setParallelEnabled(true);
|
|
300
|
+
|
|
301
|
+
// Tick 1: spawn executor
|
|
302
|
+
vi.mocked(pickNextPrompt).mockReturnValue(
|
|
303
|
+
pickerResult(makePrompt(1), { total: 3, pending: 2, inProgress: 0, done: 1, blocked: 0 }),
|
|
304
|
+
);
|
|
305
|
+
await loop.forceTick();
|
|
306
|
+
expect(callbacks.onSpawnExecutor).toHaveBeenCalledTimes(1);
|
|
307
|
+
|
|
308
|
+
// Tick 2: executor-01 visible, advance past cooldown
|
|
309
|
+
vi.mocked(listWindows).mockReturnValue([
|
|
310
|
+
{ index: 0, name: 'hub', id: '@0' },
|
|
311
|
+
{ index: 1, name: 'executor-01', id: '@1' },
|
|
312
|
+
]);
|
|
313
|
+
vi.mocked(getSpawnedAgentRegistry).mockReturnValue(new Set(['executor-01']));
|
|
314
|
+
const dateNowSpy = vi.spyOn(Date, 'now').mockReturnValue(Date.now() + 11000);
|
|
315
|
+
|
|
316
|
+
vi.mocked(pickNextPrompt).mockReturnValue(
|
|
317
|
+
pickerResult(makePrompt(2), { total: 3, pending: 1, inProgress: 1, done: 1, blocked: 0 }),
|
|
318
|
+
);
|
|
319
|
+
await loop.forceTick();
|
|
320
|
+
expect(callbacks.onSpawnExecutor).toHaveBeenCalledTimes(2);
|
|
321
|
+
|
|
322
|
+
dateNowSpy.mockRestore();
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it('resets cooldown when agent window disappears', async () => {
|
|
326
|
+
loop.setParallelEnabled(true);
|
|
327
|
+
|
|
328
|
+
// Tick 1: executor-01 already visible, spawn executor for prompt 2
|
|
329
|
+
vi.mocked(listWindows).mockReturnValue([
|
|
330
|
+
{ index: 0, name: 'hub', id: '@0' },
|
|
331
|
+
{ index: 1, name: 'executor-01', id: '@1' },
|
|
332
|
+
]);
|
|
333
|
+
vi.mocked(getSpawnedAgentRegistry).mockReturnValue(new Set(['executor-01']));
|
|
334
|
+
vi.mocked(pickNextPrompt).mockReturnValue(
|
|
335
|
+
pickerResult(makePrompt(2), { total: 3, pending: 2, inProgress: 1, done: 0, blocked: 0 }),
|
|
336
|
+
);
|
|
337
|
+
await loop.forceTick();
|
|
338
|
+
expect(callbacks.onSpawnExecutor).toHaveBeenCalledTimes(1);
|
|
339
|
+
expect(loop.getState().activeAgents).toContain('executor-01');
|
|
340
|
+
|
|
341
|
+
// Tick 2: executor-01 disappears → checkAgentWindows clears cooldown
|
|
342
|
+
// → new spawn succeeds immediately without waiting
|
|
343
|
+
vi.mocked(listWindows).mockReturnValue([{ index: 0, name: 'hub', id: '@0' }]);
|
|
344
|
+
vi.mocked(pickNextPrompt).mockReturnValue(
|
|
345
|
+
pickerResult(makePrompt(3), { total: 3, pending: 1, inProgress: 1, done: 1, blocked: 0 }),
|
|
346
|
+
);
|
|
347
|
+
await loop.forceTick();
|
|
348
|
+
|
|
349
|
+
// Cooldown was cleared by executor disappearance detection
|
|
350
|
+
expect(callbacks.onSpawnExecutor).toHaveBeenCalledTimes(2);
|
|
351
|
+
});
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
// ─── Emergent Planner Blocking ────────────────────────────────────────
|
|
355
|
+
|
|
356
|
+
describe('emergent planner blocking', () => {
|
|
357
|
+
it('blocks second emergent planner when one is already running', async () => {
|
|
358
|
+
// Emergent planner window already active
|
|
359
|
+
vi.mocked(listWindows).mockReturnValue([
|
|
360
|
+
{ index: 0, name: 'hub', id: '@0' },
|
|
361
|
+
{ index: 1, name: 'emergent-planner', id: '@1' },
|
|
362
|
+
]);
|
|
363
|
+
vi.mocked(getSpawnedAgentRegistry).mockReturnValue(new Set(['emergent-planner']));
|
|
364
|
+
|
|
365
|
+
// Would normally trigger emergent planner spawn (no pending, no in_progress)
|
|
366
|
+
vi.mocked(pickNextPrompt).mockReturnValue(
|
|
367
|
+
pickerResult(null, { total: 5, pending: 0, inProgress: 0, done: 5, blocked: 0 }),
|
|
368
|
+
);
|
|
369
|
+
|
|
370
|
+
await loop.forceTick();
|
|
371
|
+
|
|
372
|
+
expect(callbacks.onSpawnEmergentPlanning).not.toHaveBeenCalled();
|
|
373
|
+
expect(callbacks.onSpawnExecutor).not.toHaveBeenCalled();
|
|
374
|
+
});
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
// ─── activeExecutorPrompts Reconciliation ─────────────────────────────
|
|
378
|
+
|
|
379
|
+
describe('activeExecutorPrompts reconciliation', () => {
|
|
380
|
+
it('removes orphaned prompt numbers when executor window disappears', async () => {
|
|
381
|
+
// Tick 1: spawn executor for prompt 1
|
|
382
|
+
vi.mocked(pickNextPrompt).mockReturnValue(
|
|
383
|
+
pickerResult(makePrompt(1), { total: 3, pending: 1, inProgress: 0, done: 2, blocked: 0 }),
|
|
384
|
+
);
|
|
385
|
+
await loop.forceTick();
|
|
386
|
+
expect(loop.getState().activeExecutorPrompts).toEqual([1]);
|
|
387
|
+
expect(loop.getState().lastExecutorSpawnTime).not.toBeNull();
|
|
388
|
+
|
|
389
|
+
// Tick 2: no executor-01 window (it died before tmux detected it)
|
|
390
|
+
// Reconciliation in checkAgentWindows cleans up orphaned prompt number
|
|
391
|
+
vi.mocked(pickNextPrompt).mockReturnValue(
|
|
392
|
+
pickerResult(null, { total: 3, pending: 0, inProgress: 1, done: 2, blocked: 0 }, 'Waiting'),
|
|
393
|
+
);
|
|
394
|
+
await loop.forceTick();
|
|
395
|
+
|
|
396
|
+
expect(loop.getState().activeExecutorPrompts).toEqual([]);
|
|
397
|
+
expect(loop.getState().lastExecutorSpawnTime).toBeNull();
|
|
398
|
+
});
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
// ─── Emergent Planner Exponential Backoff ────────────────────────────
|
|
402
|
+
|
|
403
|
+
describe('emergent planner exponential backoff', () => {
|
|
404
|
+
/** Configure mocks for the emergent planner path (no pending, no in_progress) */
|
|
405
|
+
function setupEmergentPath(doneCount: number) {
|
|
406
|
+
const donePrompts = Array.from({ length: doneCount }, (_, i) => makePrompt(i + 1, 'done'));
|
|
407
|
+
vi.mocked(loadAllPrompts).mockReturnValue(donePrompts);
|
|
408
|
+
vi.mocked(pickNextPrompt).mockReturnValue(
|
|
409
|
+
pickerResult(null, { total: doneCount, pending: 0, inProgress: 0, done: doneCount, blocked: 0 }),
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
it('increments emergentSpawnCount on unproductive spawn and applies 20s cooldown', async () => {
|
|
414
|
+
setupEmergentPath(5);
|
|
415
|
+
|
|
416
|
+
// Tick 1: first spawn — productive (snapshot count 5 > initial emergentLastPromptCount 0)
|
|
417
|
+
await loop.forceTick();
|
|
418
|
+
expect(callbacks.onSpawnEmergentPlanning).toHaveBeenCalledTimes(1);
|
|
419
|
+
expect(loop.getState().emergentSpawnCount).toBe(0);
|
|
420
|
+
|
|
421
|
+
// Fix time at 15s after spawn — past base 10s but under 20s backoff
|
|
422
|
+
const spawnTime = loop.getState().lastExecutorSpawnTime!;
|
|
423
|
+
const dateNowSpy = vi.spyOn(Date, 'now').mockReturnValue(spawnTime + 15000);
|
|
424
|
+
|
|
425
|
+
// Tick 2: unproductive (count still 5) → emergentSpawnCount = 1, cooldown = 20s
|
|
426
|
+
await loop.forceTick();
|
|
427
|
+
expect(callbacks.onSpawnEmergentPlanning).toHaveBeenCalledTimes(1); // blocked by backoff
|
|
428
|
+
expect(loop.getState().emergentSpawnCount).toBe(1);
|
|
429
|
+
expect(callbacks.onLoopStatus).toHaveBeenCalledWith(
|
|
430
|
+
'Emergent planner backoff: waiting 20s (1 unproductive spawns)',
|
|
431
|
+
);
|
|
432
|
+
|
|
433
|
+
dateNowSpy.mockRestore();
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
it('doubles cooldown with each unproductive attempt: 20s, 40s, 80s, 160s, capped at 160s', async () => {
|
|
437
|
+
setupEmergentPath(5);
|
|
438
|
+
|
|
439
|
+
// Tick 1: first spawn (productive, emergentSpawnCount = 0)
|
|
440
|
+
await loop.forceTick();
|
|
441
|
+
expect(callbacks.onSpawnEmergentPlanning).toHaveBeenCalledTimes(1);
|
|
442
|
+
|
|
443
|
+
// Advance time past base 10s cooldown but within each escalating backoff window
|
|
444
|
+
const spawnTime = loop.getState().lastExecutorSpawnTime!;
|
|
445
|
+
const dateNowSpy = vi.spyOn(Date, 'now');
|
|
446
|
+
|
|
447
|
+
dateNowSpy.mockReturnValue(spawnTime + 11000); // 11s: past base 10s, within 20s
|
|
448
|
+
await loop.forceTick(); // count → 1
|
|
449
|
+
expect(callbacks.onLoopStatus).toHaveBeenCalledWith(
|
|
450
|
+
'Emergent planner backoff: waiting 20s (1 unproductive spawns)',
|
|
451
|
+
);
|
|
452
|
+
|
|
453
|
+
dateNowSpy.mockReturnValue(spawnTime + 21000); // 21s: past base, within 40s
|
|
454
|
+
await loop.forceTick(); // count → 2
|
|
455
|
+
expect(callbacks.onLoopStatus).toHaveBeenCalledWith(
|
|
456
|
+
'Emergent planner backoff: waiting 40s (2 unproductive spawns)',
|
|
457
|
+
);
|
|
458
|
+
|
|
459
|
+
dateNowSpy.mockReturnValue(spawnTime + 41000); // 41s: past base, within 80s
|
|
460
|
+
await loop.forceTick(); // count → 3
|
|
461
|
+
expect(callbacks.onLoopStatus).toHaveBeenCalledWith(
|
|
462
|
+
'Emergent planner backoff: waiting 80s (3 unproductive spawns)',
|
|
463
|
+
);
|
|
464
|
+
|
|
465
|
+
dateNowSpy.mockReturnValue(spawnTime + 81000); // 81s: past base, within 160s
|
|
466
|
+
await loop.forceTick(); // count → 4
|
|
467
|
+
expect(callbacks.onLoopStatus).toHaveBeenCalledWith(
|
|
468
|
+
'Emergent planner backoff: waiting 160s (4 unproductive spawns)',
|
|
469
|
+
);
|
|
470
|
+
|
|
471
|
+
// count → 5, Math.min(5, 4) = 4, cooldown still 160s (capped at same time offset)
|
|
472
|
+
await loop.forceTick();
|
|
473
|
+
expect(callbacks.onLoopStatus).toHaveBeenCalledWith(
|
|
474
|
+
'Emergent planner backoff: waiting 160s (5 unproductive spawns)',
|
|
475
|
+
);
|
|
476
|
+
|
|
477
|
+
// Only the initial spawn succeeded — all subsequent ticks blocked
|
|
478
|
+
expect(callbacks.onSpawnEmergentPlanning).toHaveBeenCalledTimes(1);
|
|
479
|
+
|
|
480
|
+
dateNowSpy.mockRestore();
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
it('resets backoff when new pending prompts appear externally', async () => {
|
|
484
|
+
setupEmergentPath(5);
|
|
485
|
+
|
|
486
|
+
// Tick 1: spawn emergent (productive, count = 0)
|
|
487
|
+
await loop.forceTick();
|
|
488
|
+
expect(callbacks.onSpawnEmergentPlanning).toHaveBeenCalledTimes(1);
|
|
489
|
+
|
|
490
|
+
// Tick 2 at +11s: unproductive → count = 1, backoff (cooldown 20s, 11s elapsed)
|
|
491
|
+
const spawnTime = loop.getState().lastExecutorSpawnTime!;
|
|
492
|
+
const dateNowSpy = vi.spyOn(Date, 'now').mockReturnValue(spawnTime + 11000);
|
|
493
|
+
await loop.forceTick();
|
|
494
|
+
expect(loop.getState().emergentSpawnCount).toBe(1);
|
|
495
|
+
|
|
496
|
+
// External prompt appears: pending count increases from 0 to 1
|
|
497
|
+
const donePrompts = Array.from({ length: 5 }, (_, i) => makePrompt(i + 1, 'done'));
|
|
498
|
+
const pendingPrompt = makePrompt(6, 'pending');
|
|
499
|
+
vi.mocked(loadAllPrompts).mockReturnValue([...donePrompts, pendingPrompt]);
|
|
500
|
+
vi.mocked(pickNextPrompt).mockReturnValue(
|
|
501
|
+
pickerResult(pendingPrompt, { total: 6, pending: 1, inProgress: 0, done: 5, blocked: 0 }),
|
|
502
|
+
);
|
|
503
|
+
|
|
504
|
+
// Tick 3: checkPromptFiles detects pending increase → resets emergentSpawnCount
|
|
505
|
+
// Then checkPromptLoop picks pending prompt → spawns executor
|
|
506
|
+
await loop.forceTick();
|
|
507
|
+
expect(loop.getState().emergentSpawnCount).toBe(0);
|
|
508
|
+
expect(callbacks.onSpawnExecutor).toHaveBeenCalledWith(pendingPrompt);
|
|
509
|
+
|
|
510
|
+
dateNowSpy.mockRestore();
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
it('resets backoff when emergent planner produces new prompts', async () => {
|
|
514
|
+
setupEmergentPath(5);
|
|
515
|
+
|
|
516
|
+
// Tick 1: spawn emergent (productive, count = 0)
|
|
517
|
+
await loop.forceTick();
|
|
518
|
+
expect(callbacks.onSpawnEmergentPlanning).toHaveBeenCalledTimes(1);
|
|
519
|
+
|
|
520
|
+
// Tick 2 at +11s: past base cooldown, enters emergent path — unproductive → count = 1
|
|
521
|
+
const spawnTime = loop.getState().lastExecutorSpawnTime!;
|
|
522
|
+
const dateNowSpy = vi.spyOn(Date, 'now').mockReturnValue(spawnTime + 11000);
|
|
523
|
+
await loop.forceTick();
|
|
524
|
+
expect(loop.getState().emergentSpawnCount).toBe(1);
|
|
525
|
+
|
|
526
|
+
// Emergent planner produced a new prompt (total count increases 5 → 6)
|
|
527
|
+
setupEmergentPath(6);
|
|
528
|
+
|
|
529
|
+
// Tick 3 at +21s: past base cooldown, productive (count 6 > emergentLastPromptCount 5)
|
|
530
|
+
// → count = 0, cooldown = 10s base, 21s elapsed → spawns
|
|
531
|
+
dateNowSpy.mockReturnValue(spawnTime + 21000);
|
|
532
|
+
await loop.forceTick();
|
|
533
|
+
expect(loop.getState().emergentSpawnCount).toBe(0);
|
|
534
|
+
expect(callbacks.onSpawnEmergentPlanning).toHaveBeenCalledTimes(2);
|
|
535
|
+
|
|
536
|
+
dateNowSpy.mockRestore();
|
|
537
|
+
});
|
|
538
|
+
});
|
|
539
|
+
});
|