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,1051 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tmux Integration
|
|
3
|
+
*
|
|
4
|
+
* Manages tmux sessions and windows for agent spawning.
|
|
5
|
+
*
|
|
6
|
+
* Session Structure:
|
|
7
|
+
* - Session: ah-hub (standardized name)
|
|
8
|
+
* - Window 0: TUI (main control)
|
|
9
|
+
* - Window 1+: Agent windows (coordinator, planner, executor, etc.)
|
|
10
|
+
*
|
|
11
|
+
* Startup Logic:
|
|
12
|
+
* 1. Check if tmux is available (fail with error if not)
|
|
13
|
+
* 2. If NOT in tmux: Create new session with user-provided name
|
|
14
|
+
* 3. If in tmux with multiple windows: Ask to create new session or use current
|
|
15
|
+
* 4. If in tmux with single window: Use current session
|
|
16
|
+
* 5. Rename active session to "ah-hub"
|
|
17
|
+
*
|
|
18
|
+
* Environment Variables passed to agents:
|
|
19
|
+
* - AGENT_ID: Unique agent identifier (= window name, used for MCP daemon isolation)
|
|
20
|
+
* - AGENT_TYPE: executor, coordinator, planner, judge, ideation, pr-reviewer
|
|
21
|
+
* - PROMPT_NUMBER: Current prompt number (when applicable)
|
|
22
|
+
* - SPEC_NAME: Current spec name
|
|
23
|
+
* - BRANCH: Current git branch
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { execSync, spawn } from 'child_process';
|
|
27
|
+
import { existsSync, readFileSync, mkdirSync, writeFileSync, readdirSync, unlinkSync, statSync } from 'fs';
|
|
28
|
+
import { join } from 'path';
|
|
29
|
+
import {
|
|
30
|
+
buildAgentInvocation,
|
|
31
|
+
listAgentProfiles,
|
|
32
|
+
loadAgentProfile,
|
|
33
|
+
type TemplateContext
|
|
34
|
+
} from './opencode/index.js';
|
|
35
|
+
import { getCurrentBranch, getPlanningPaths } from './planning.js';
|
|
36
|
+
import { getBaseBranch } from './git.js';
|
|
37
|
+
import { addSpawnedWindow, removeSpawnedWindow, getSpawnedWindows } from './session.js';
|
|
38
|
+
import { loadProjectSettings } from '../hooks/shared.js';
|
|
39
|
+
import { getSpecForBranch, getWorkflowDomain } from './specs.js';
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Agent type = agent profile name.
|
|
43
|
+
* Derived from .allhands/agents/*.yaml profile files.
|
|
44
|
+
*/
|
|
45
|
+
export type AgentType = string;
|
|
46
|
+
|
|
47
|
+
export interface AgentEnv {
|
|
48
|
+
AGENT_ID: string;
|
|
49
|
+
AGENT_TYPE: AgentType;
|
|
50
|
+
PROMPT_NUMBER?: string;
|
|
51
|
+
SPEC_NAME?: string;
|
|
52
|
+
BRANCH: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface SpawnConfig {
|
|
56
|
+
name: string;
|
|
57
|
+
agentType: AgentType;
|
|
58
|
+
flowPath: string;
|
|
59
|
+
preamble?: string;
|
|
60
|
+
promptNumber?: number;
|
|
61
|
+
specName?: string;
|
|
62
|
+
nonCoding?: boolean;
|
|
63
|
+
/** If true, switch focus to the new window after spawning (default: true for TUI actions) */
|
|
64
|
+
focusWindow?: boolean;
|
|
65
|
+
/**
|
|
66
|
+
* If true, this agent is scoped to a specific prompt and can have multiple
|
|
67
|
+
* instances running concurrently (one per prompt).
|
|
68
|
+
* Prompt-scoped agents include the prompt number in their ID (e.g., "executor-01").
|
|
69
|
+
* Non-prompt-scoped agents use their name as AGENT_ID and only one can run at a time.
|
|
70
|
+
*/
|
|
71
|
+
promptScoped?: boolean;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface SessionContext {
|
|
75
|
+
inTmux: boolean;
|
|
76
|
+
currentSession: string | null;
|
|
77
|
+
windowCount: number;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface SessionSetupResult {
|
|
81
|
+
sessionName: string;
|
|
82
|
+
isNew: boolean;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export const SESSION_NAME = 'ah-hub';
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* In-memory cache of agents spawned by ALL HANDS.
|
|
89
|
+
* Also persisted to session.json for cross-process visibility.
|
|
90
|
+
*/
|
|
91
|
+
const spawnedAgentRegistry = new Set<string>();
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Clean up old launcher scripts (older than 24 hours)
|
|
95
|
+
*/
|
|
96
|
+
function cleanupOldLaunchers(launcherDir: string): void {
|
|
97
|
+
if (!existsSync(launcherDir)) return;
|
|
98
|
+
|
|
99
|
+
const maxAge = 24 * 60 * 60 * 1000; // 24 hours
|
|
100
|
+
const now = Date.now();
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
const files = readdirSync(launcherDir);
|
|
104
|
+
for (const file of files) {
|
|
105
|
+
if (!file.endsWith('-launcher.sh') && !file.endsWith('-prompt.txt')) continue;
|
|
106
|
+
|
|
107
|
+
const filePath = join(launcherDir, file);
|
|
108
|
+
try {
|
|
109
|
+
const stat = statSync(filePath);
|
|
110
|
+
if (now - stat.mtimeMs > maxAge) {
|
|
111
|
+
unlinkSync(filePath);
|
|
112
|
+
}
|
|
113
|
+
} catch {
|
|
114
|
+
// Ignore errors for individual files
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
} catch {
|
|
118
|
+
// Ignore errors during cleanup
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Register an agent as spawned by ALL HANDS (persisted to disk)
|
|
124
|
+
*/
|
|
125
|
+
export function registerSpawnedAgent(windowName: string, cwd?: string): void {
|
|
126
|
+
spawnedAgentRegistry.add(windowName);
|
|
127
|
+
addSpawnedWindow(windowName, cwd);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Unregister an agent (persisted to disk)
|
|
132
|
+
*/
|
|
133
|
+
export function unregisterSpawnedAgent(windowName: string, cwd?: string): void {
|
|
134
|
+
spawnedAgentRegistry.delete(windowName);
|
|
135
|
+
removeSpawnedWindow(windowName, cwd);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Check if an agent was spawned by ALL HANDS
|
|
140
|
+
*/
|
|
141
|
+
export function isSpawnedAgent(windowName: string, cwd?: string): boolean {
|
|
142
|
+
// Check both in-memory cache and persisted state
|
|
143
|
+
if (spawnedAgentRegistry.has(windowName)) return true;
|
|
144
|
+
return getSpawnedWindows(cwd).includes(windowName);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Get all registered spawned agents (from persisted state)
|
|
149
|
+
*/
|
|
150
|
+
export function getSpawnedAgentRegistry(cwd?: string): Set<string> {
|
|
151
|
+
// Merge in-memory and persisted for complete view
|
|
152
|
+
const persisted = getSpawnedWindows(cwd);
|
|
153
|
+
return new Set([...spawnedAgentRegistry, ...persisted]);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Get current tmux window ID (stable identifier like @0)
|
|
158
|
+
*/
|
|
159
|
+
export function getCurrentWindowId(): string | null {
|
|
160
|
+
if (!process.env.TMUX) return null;
|
|
161
|
+
try {
|
|
162
|
+
return execSync('tmux display-message -p "#{window_id}"', {
|
|
163
|
+
encoding: 'utf-8',
|
|
164
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
165
|
+
}).trim();
|
|
166
|
+
} catch {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Get current tmux context (are we in tmux, which session, how many windows)
|
|
173
|
+
*/
|
|
174
|
+
export function getTmuxContext(): SessionContext {
|
|
175
|
+
const inTmux = !!process.env.TMUX;
|
|
176
|
+
|
|
177
|
+
if (!inTmux) {
|
|
178
|
+
return { inTmux: false, currentSession: null, windowCount: 0 };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
const currentSession = execSync('tmux display-message -p "#S"', {
|
|
183
|
+
encoding: 'utf-8',
|
|
184
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
185
|
+
}).trim();
|
|
186
|
+
|
|
187
|
+
const windowList = execSync(
|
|
188
|
+
`tmux list-windows -t "${currentSession}" -F "#{window_index}"`,
|
|
189
|
+
{ encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }
|
|
190
|
+
);
|
|
191
|
+
const windowCount = windowList.trim().split('\n').filter((l) => l).length;
|
|
192
|
+
|
|
193
|
+
return { inTmux: true, currentSession, windowCount };
|
|
194
|
+
} catch {
|
|
195
|
+
return { inTmux: true, currentSession: null, windowCount: 0 };
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Check if tmux needs to prompt user for session decision
|
|
201
|
+
* Returns: 'create-new' | 'use-current' | 'no-prompt-needed'
|
|
202
|
+
*/
|
|
203
|
+
export function getSessionDecision(context: SessionContext): 'create-new' | 'use-current' | 'no-prompt-needed' {
|
|
204
|
+
if (!context.inTmux) {
|
|
205
|
+
// Not in tmux - will need to create new
|
|
206
|
+
return 'create-new';
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (context.windowCount > 1) {
|
|
210
|
+
// Multiple windows - should ask user
|
|
211
|
+
// This will be handled by TUI prompting
|
|
212
|
+
return 'create-new'; // Default to create-new, TUI can override
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Single window in tmux - use current
|
|
216
|
+
return 'use-current';
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Rename current session to ah-hub
|
|
221
|
+
*/
|
|
222
|
+
export function renameCurrentSession(): void {
|
|
223
|
+
try {
|
|
224
|
+
execSync(`tmux rename-session "${SESSION_NAME}"`, { stdio: 'pipe' });
|
|
225
|
+
} catch {
|
|
226
|
+
// Session might already be named this
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Create a new tmux session and attach to it
|
|
232
|
+
*/
|
|
233
|
+
export function createNewSession(sessionName: string, cwd?: string): void {
|
|
234
|
+
const cwdArg = cwd ? `-c "${cwd}"` : '';
|
|
235
|
+
execSync(`tmux new-session -d -s "${sessionName}" ${cwdArg}`, { stdio: 'pipe' });
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Setup TUI session with the new logic
|
|
240
|
+
*
|
|
241
|
+
* @param promptForNewSession - Callback to ask user if they want a new session
|
|
242
|
+
* @param promptForSessionName - Callback to get session name from user
|
|
243
|
+
* @param cwd - Working directory
|
|
244
|
+
* @returns Session setup result
|
|
245
|
+
*/
|
|
246
|
+
export async function setupTUISession(
|
|
247
|
+
promptForNewSession: () => Promise<boolean>,
|
|
248
|
+
promptForSessionName: () => Promise<string>,
|
|
249
|
+
cwd?: string
|
|
250
|
+
): Promise<SessionSetupResult> {
|
|
251
|
+
// Step 1: Check tmux availability
|
|
252
|
+
if (!isTmuxInstalled()) {
|
|
253
|
+
throw new Error('tmux is required but not found. Please install tmux and try again.');
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const context = getTmuxContext();
|
|
257
|
+
|
|
258
|
+
// Step 2: Determine session strategy
|
|
259
|
+
if (!context.inTmux) {
|
|
260
|
+
// Not in tmux - create new session
|
|
261
|
+
const name = await promptForSessionName();
|
|
262
|
+
createNewSession(name, cwd);
|
|
263
|
+
// Attach and rename
|
|
264
|
+
execSync(`tmux rename-session -t "${name}" "${SESSION_NAME}"`, { stdio: 'pipe' });
|
|
265
|
+
return { sessionName: SESSION_NAME, isNew: true };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (context.windowCount > 1) {
|
|
269
|
+
// Multiple windows - ask user
|
|
270
|
+
const wantNew = await promptForNewSession();
|
|
271
|
+
if (wantNew) {
|
|
272
|
+
const name = await promptForSessionName();
|
|
273
|
+
createNewSession(name, cwd);
|
|
274
|
+
execSync(`tmux rename-session -t "${name}" "${SESSION_NAME}"`, { stdio: 'pipe' });
|
|
275
|
+
// Switch to new session
|
|
276
|
+
execSync(`tmux switch-client -t "${SESSION_NAME}"`, { stdio: 'pipe' });
|
|
277
|
+
return { sessionName: SESSION_NAME, isNew: true };
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Use current session - just rename it
|
|
282
|
+
renameCurrentSession();
|
|
283
|
+
return { sessionName: SESSION_NAME, isNew: false };
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Get the session name for the current branch (legacy - kept for compatibility)
|
|
288
|
+
*/
|
|
289
|
+
export function getSessionName(branch?: string): string {
|
|
290
|
+
// Now always returns ah-hub for active session
|
|
291
|
+
return SESSION_NAME;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Check if tmux is installed
|
|
296
|
+
*/
|
|
297
|
+
export function isTmuxInstalled(): boolean {
|
|
298
|
+
try {
|
|
299
|
+
execSync('which tmux', { stdio: 'pipe' });
|
|
300
|
+
return true;
|
|
301
|
+
} catch {
|
|
302
|
+
return false;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Check if a tmux session exists
|
|
308
|
+
*/
|
|
309
|
+
export function sessionExists(sessionName: string): boolean {
|
|
310
|
+
try {
|
|
311
|
+
execSync(`tmux has-session -t "${sessionName}" 2>/dev/null`, { stdio: 'pipe' });
|
|
312
|
+
return true;
|
|
313
|
+
} catch {
|
|
314
|
+
return false;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Create a new tmux session
|
|
320
|
+
*/
|
|
321
|
+
export function createSession(sessionName: string, cwd?: string): void {
|
|
322
|
+
const cwdArg = cwd ? `-c "${cwd}"` : '';
|
|
323
|
+
execSync(`tmux new-session -d -s "${sessionName}" ${cwdArg}`, { stdio: 'pipe' });
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Get the current tmux session name (if inside tmux)
|
|
328
|
+
*/
|
|
329
|
+
export function getCurrentSession(): string | null {
|
|
330
|
+
if (!process.env.TMUX) {
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
try {
|
|
335
|
+
return execSync('tmux display-message -p "#S"', {
|
|
336
|
+
encoding: 'utf-8',
|
|
337
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
338
|
+
}).trim();
|
|
339
|
+
} catch {
|
|
340
|
+
return null;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Ensure session exists, creating if necessary
|
|
346
|
+
*
|
|
347
|
+
* IMPORTANT: If already inside tmux, uses the CURRENT session.
|
|
348
|
+
* Only creates a new session if not inside tmux.
|
|
349
|
+
*/
|
|
350
|
+
export function ensureSession(branch?: string, cwd?: string): string {
|
|
351
|
+
// If we're already in tmux, use the current session
|
|
352
|
+
const currentSession = getCurrentSession();
|
|
353
|
+
if (currentSession) {
|
|
354
|
+
return currentSession;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Not in tmux - check if our target session exists, or create it
|
|
358
|
+
const sessionName = getSessionName(branch);
|
|
359
|
+
|
|
360
|
+
if (!sessionExists(sessionName)) {
|
|
361
|
+
createSession(sessionName, cwd);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return sessionName;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* List windows in a session
|
|
369
|
+
*/
|
|
370
|
+
export function listWindows(sessionName: string): Array<{ index: number; name: string; id: string }> {
|
|
371
|
+
try {
|
|
372
|
+
const output = execSync(
|
|
373
|
+
`tmux list-windows -t "${sessionName}" -F "#{window_index}:#{window_name}:#{window_id}"`,
|
|
374
|
+
{ encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }
|
|
375
|
+
);
|
|
376
|
+
|
|
377
|
+
return output
|
|
378
|
+
.trim()
|
|
379
|
+
.split('\n')
|
|
380
|
+
.filter((line) => line.length > 0)
|
|
381
|
+
.map((line) => {
|
|
382
|
+
const parts = line.split(':');
|
|
383
|
+
const index = parseInt(parts[0], 10);
|
|
384
|
+
const name = parts[1];
|
|
385
|
+
const id = parts[2] || '';
|
|
386
|
+
return { index, name, id };
|
|
387
|
+
});
|
|
388
|
+
} catch {
|
|
389
|
+
return [];
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Check if a window with given name exists
|
|
395
|
+
*/
|
|
396
|
+
export function windowExists(sessionName: string, windowName: string): boolean {
|
|
397
|
+
const windows = listWindows(sessionName);
|
|
398
|
+
return windows.some((w) => w.name === windowName);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Create a new window in the session
|
|
403
|
+
*
|
|
404
|
+
* @param sessionName - Target session
|
|
405
|
+
* @param windowName - Name for the new window
|
|
406
|
+
* @param cwd - Working directory
|
|
407
|
+
* @param detached - If true, don't switch focus to new window (default: true)
|
|
408
|
+
*/
|
|
409
|
+
export function createWindow(
|
|
410
|
+
sessionName: string,
|
|
411
|
+
windowName: string,
|
|
412
|
+
cwd?: string,
|
|
413
|
+
detached: boolean = true
|
|
414
|
+
): number {
|
|
415
|
+
const cwdArg = cwd ? `-c "${cwd}"` : '';
|
|
416
|
+
const detachArg = detached ? '-d' : '';
|
|
417
|
+
execSync(`tmux new-window ${detachArg} -t "${sessionName}" -n "${windowName}" ${cwdArg}`, {
|
|
418
|
+
stdio: 'pipe',
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
// Prevent tmux from overriding the window name via automatic-rename
|
|
422
|
+
try {
|
|
423
|
+
execSync(`tmux set-option -t "${sessionName}:${windowName}" allow-rename off`, {
|
|
424
|
+
stdio: 'pipe',
|
|
425
|
+
});
|
|
426
|
+
} catch {
|
|
427
|
+
// Ignore — non-critical
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Get the new window's index
|
|
431
|
+
const windows = listWindows(sessionName);
|
|
432
|
+
const window = windows.find((w) => w.name === windowName);
|
|
433
|
+
return window?.index ?? -1;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Kill a window by name
|
|
438
|
+
*/
|
|
439
|
+
export function killWindow(sessionName: string, windowName: string): boolean {
|
|
440
|
+
try {
|
|
441
|
+
execSync(`tmux kill-window -t "${sessionName}:${windowName}"`, { stdio: 'pipe' });
|
|
442
|
+
// Unregister from ALL HANDS tracking
|
|
443
|
+
unregisterSpawnedAgent(windowName);
|
|
444
|
+
return true;
|
|
445
|
+
} catch {
|
|
446
|
+
return false;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Send keys to a window
|
|
452
|
+
*/
|
|
453
|
+
export function sendKeys(sessionName: string, windowName: string, keys: string): void {
|
|
454
|
+
execSync(`tmux send-keys -t "${sessionName}:${windowName}" "${keys}" Enter`, {
|
|
455
|
+
stdio: 'pipe',
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Capture window output
|
|
461
|
+
*/
|
|
462
|
+
export function capturePane(
|
|
463
|
+
sessionName: string,
|
|
464
|
+
windowName: string,
|
|
465
|
+
lines: number = 100
|
|
466
|
+
): string {
|
|
467
|
+
try {
|
|
468
|
+
return execSync(
|
|
469
|
+
`tmux capture-pane -t "${sessionName}:${windowName}" -p -S -${lines}`,
|
|
470
|
+
{ encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }
|
|
471
|
+
);
|
|
472
|
+
} catch {
|
|
473
|
+
return '';
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Select/focus a window
|
|
479
|
+
*/
|
|
480
|
+
export function selectWindow(sessionName: string, windowName: string): void {
|
|
481
|
+
execSync(`tmux select-window -t "${sessionName}:${windowName}"`, { stdio: 'pipe' });
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Rename the current window
|
|
486
|
+
*/
|
|
487
|
+
export function renameCurrentWindow(newName: string): void {
|
|
488
|
+
if (!process.env.TMUX) return;
|
|
489
|
+
try {
|
|
490
|
+
execSync(`tmux rename-window "${newName}"`, { stdio: 'pipe' });
|
|
491
|
+
} catch {
|
|
492
|
+
// Ignore errors
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Rename a specific window by ID (stable even if focus changes)
|
|
498
|
+
* Falls back to renaming current window if no target specified
|
|
499
|
+
*/
|
|
500
|
+
export function renameWindow(targetWindowId: string | null, newName: string): void {
|
|
501
|
+
if (!process.env.TMUX) return;
|
|
502
|
+
try {
|
|
503
|
+
if (targetWindowId) {
|
|
504
|
+
// Use -t to target the specific window by ID
|
|
505
|
+
execSync(`tmux rename-window -t "${targetWindowId}" "${newName}"`, { stdio: 'pipe' });
|
|
506
|
+
} else {
|
|
507
|
+
// Fallback to current window
|
|
508
|
+
execSync(`tmux rename-window "${newName}"`, { stdio: 'pipe' });
|
|
509
|
+
}
|
|
510
|
+
} catch {
|
|
511
|
+
// Ignore errors
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* Build the window name for an agent.
|
|
517
|
+
*
|
|
518
|
+
* Non-prompt-scoped agents use their name directly (e.g., "planner").
|
|
519
|
+
* Prompt-scoped agents include the prompt number (e.g., "executor-01").
|
|
520
|
+
*/
|
|
521
|
+
export function buildWindowName(config: SpawnConfig): string {
|
|
522
|
+
if (!config.promptScoped) {
|
|
523
|
+
return config.name;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// Prompt-scoped agents include prompt number
|
|
527
|
+
if (config.promptNumber !== undefined) {
|
|
528
|
+
return `${config.name}-${String(config.promptNumber).padStart(2, '0')}`;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// Fallback: use name as-is
|
|
532
|
+
return config.name;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* Build environment variables for agent
|
|
537
|
+
*/
|
|
538
|
+
export function buildAgentEnv(config: SpawnConfig, branch: string, windowName: string): Record<string, string> {
|
|
539
|
+
// Note: BASE_BRANCH is communicated via the initial prompt, not env vars
|
|
540
|
+
const env: Record<string, string> = {
|
|
541
|
+
AGENT_ID: windowName, // Window name = AGENT_ID (used for MCP daemon isolation)
|
|
542
|
+
AGENT_TYPE: config.agentType,
|
|
543
|
+
BRANCH: branch,
|
|
544
|
+
};
|
|
545
|
+
|
|
546
|
+
if (config.promptScoped) {
|
|
547
|
+
env.PROMPT_SCOPED = 'true';
|
|
548
|
+
|
|
549
|
+
// Set autocompact threshold for prompt-scoped agents only
|
|
550
|
+
const settings = loadProjectSettings();
|
|
551
|
+
const autocompactAt = settings?.spawn?.promptScopedAutocompactAt ?? 65;
|
|
552
|
+
env.CLAUDE_AUTOCOMPACT_PCT_OVERRIDE = String(autocompactAt);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
if (config.promptNumber !== undefined) {
|
|
556
|
+
env.PROMPT_NUMBER = String(config.promptNumber).padStart(2, '0');
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
if (config.specName) {
|
|
560
|
+
env.SPEC_NAME = config.specName;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
return env;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* Spawn a Claude Code agent in a tmux window
|
|
568
|
+
*
|
|
569
|
+
* This creates a new window and runs `claude` with the appropriate
|
|
570
|
+
* flow and configuration.
|
|
571
|
+
*
|
|
572
|
+
* @param config - Agent spawn configuration
|
|
573
|
+
* @param branch - Git branch (defaults to current)
|
|
574
|
+
* @param cwd - Working directory
|
|
575
|
+
* @returns Session and window names
|
|
576
|
+
* @throws Error if non-prompt-scoped agent already exists
|
|
577
|
+
*
|
|
578
|
+
* Window naming:
|
|
579
|
+
* - Non-prompt-scoped agents use name directly (e.g., "planner")
|
|
580
|
+
* - Prompt-scoped agents include prompt number (e.g., "executor-01")
|
|
581
|
+
*
|
|
582
|
+
* The window name becomes the AGENT_ID for MCP daemon isolation.
|
|
583
|
+
*
|
|
584
|
+
* Window focus behavior:
|
|
585
|
+
* - config.focusWindow = true (default): Switch to the new window after spawning
|
|
586
|
+
* - config.focusWindow = false: Create window in background (for loop-spawned executors)
|
|
587
|
+
*/
|
|
588
|
+
export function spawnAgent(
|
|
589
|
+
config: SpawnConfig,
|
|
590
|
+
branch?: string,
|
|
591
|
+
cwd?: string
|
|
592
|
+
): { sessionName: string; windowName: string } {
|
|
593
|
+
const currentBranch = branch || getCurrentBranch();
|
|
594
|
+
const sessionName = ensureSession(currentBranch, cwd);
|
|
595
|
+
const windowName = buildWindowName(config);
|
|
596
|
+
const shouldFocus = config.focusWindow !== false; // Default to true
|
|
597
|
+
|
|
598
|
+
// Non-prompt-scoped agent enforcement: fail if already running
|
|
599
|
+
if (!config.promptScoped && windowExists(sessionName, windowName)) {
|
|
600
|
+
throw new Error(
|
|
601
|
+
`Agent "${windowName}" is already running. Only one instance of non-prompt-scoped agents is allowed.`
|
|
602
|
+
);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// Kill existing window if present (for prompt-scoped agents being restarted)
|
|
606
|
+
if (windowExists(sessionName, windowName)) {
|
|
607
|
+
killWindow(sessionName, windowName);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// Create new window (detached - don't switch focus yet)
|
|
611
|
+
createWindow(sessionName, windowName, cwd, true);
|
|
612
|
+
|
|
613
|
+
// Register this agent as spawned by ALL HANDS
|
|
614
|
+
registerSpawnedAgent(windowName);
|
|
615
|
+
|
|
616
|
+
// Build environment variables for the agent
|
|
617
|
+
const env = buildAgentEnv(config, currentBranch, windowName);
|
|
618
|
+
|
|
619
|
+
// Read the flow file content directly instead of referencing it
|
|
620
|
+
let flowContent = '';
|
|
621
|
+
if (existsSync(config.flowPath)) {
|
|
622
|
+
flowContent = readFileSync(config.flowPath, 'utf-8');
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// Write a launcher script to avoid all shell escaping issues
|
|
626
|
+
const tempDir = join(cwd || process.cwd(), '.allhands', 'harness', '.cache', 'launchers');
|
|
627
|
+
mkdirSync(tempDir, { recursive: true });
|
|
628
|
+
|
|
629
|
+
// Clean up old launcher files (older than 24h)
|
|
630
|
+
cleanupOldLaunchers(tempDir);
|
|
631
|
+
|
|
632
|
+
const launcherScript = join(tempDir, `${windowName}-launcher.sh`);
|
|
633
|
+
const promptFile = join(tempDir, `${windowName}-prompt.txt`);
|
|
634
|
+
|
|
635
|
+
// Build combined prompt: flow content + preamble + base branch info
|
|
636
|
+
// NO system prompt - everything goes into the initial user prompt
|
|
637
|
+
const baseBranch = getBaseBranch();
|
|
638
|
+
const promptParts: string[] = [];
|
|
639
|
+
|
|
640
|
+
if (flowContent) {
|
|
641
|
+
promptParts.push(flowContent);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
if (config.preamble && config.preamble.trim()) {
|
|
645
|
+
promptParts.push(config.preamble);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// Always append base branch info
|
|
649
|
+
promptParts.push(`The base branch name is "${baseBranch}".`);
|
|
650
|
+
|
|
651
|
+
const combinedPrompt = promptParts.join('\n\n');
|
|
652
|
+
writeFileSync(promptFile, combinedPrompt, 'utf-8');
|
|
653
|
+
|
|
654
|
+
// Build the launcher script
|
|
655
|
+
const scriptLines: string[] = ['#!/bin/bash', ''];
|
|
656
|
+
|
|
657
|
+
// Skip pyenv rehash to avoid lock contention when spawning multiple agents
|
|
658
|
+
scriptLines.push('export PYENV_REHASH_SKIP=1');
|
|
659
|
+
|
|
660
|
+
// Export environment variables
|
|
661
|
+
for (const [key, value] of Object.entries(env)) {
|
|
662
|
+
scriptLines.push(`export ${key}="${value}"`);
|
|
663
|
+
}
|
|
664
|
+
scriptLines.push('');
|
|
665
|
+
|
|
666
|
+
// Build claude command - NO system prompt, everything in initial prompt
|
|
667
|
+
const cmdParts: string[] = ['claude'];
|
|
668
|
+
cmdParts.push('--settings .claude/settings.json');
|
|
669
|
+
cmdParts.push('--dangerously-skip-permissions');
|
|
670
|
+
cmdParts.push(`"$(cat '${promptFile}')"`)
|
|
671
|
+
|
|
672
|
+
scriptLines.push(cmdParts.join(' \\\n '));
|
|
673
|
+
|
|
674
|
+
writeFileSync(launcherScript, scriptLines.join('\n'), { mode: 0o755 });
|
|
675
|
+
|
|
676
|
+
// Execute the launcher script with exec so it replaces the shell.
|
|
677
|
+
// This ensures the window closes when claude exits (no orphan shell).
|
|
678
|
+
sendKeys(sessionName, windowName, `exec bash '${launcherScript}'`);
|
|
679
|
+
|
|
680
|
+
// Switch focus to the new window if requested (default for TUI actions)
|
|
681
|
+
if (shouldFocus) {
|
|
682
|
+
selectWindow(sessionName, windowName);
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
return { sessionName, windowName };
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
/**
|
|
689
|
+
* Configuration for profile-based agent spawning
|
|
690
|
+
*/
|
|
691
|
+
export interface ProfileSpawnConfig {
|
|
692
|
+
/** Agent profile name (must exist in .allhands/agents/) */
|
|
693
|
+
agentName: string;
|
|
694
|
+
/** Template context for variable resolution */
|
|
695
|
+
context: TemplateContext;
|
|
696
|
+
/** Optional prompt number for prompt-scoped agents */
|
|
697
|
+
promptNumber?: number;
|
|
698
|
+
/** If true, switch focus to the new window (default: true) */
|
|
699
|
+
focusWindow?: boolean;
|
|
700
|
+
/** Optional flow path override — when provided, use this instead of the profile's default flow */
|
|
701
|
+
flowOverride?: string;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
/**
|
|
705
|
+
* Spawn an agent using its profile definition
|
|
706
|
+
*
|
|
707
|
+
* This is the preferred way to spawn agents. It:
|
|
708
|
+
* 1. Loads the agent profile
|
|
709
|
+
* 2. Validates required template variables
|
|
710
|
+
* 3. Resolves the message template
|
|
711
|
+
* 4. Spawns the agent with proper configuration
|
|
712
|
+
*
|
|
713
|
+
* @param config - Profile spawn configuration
|
|
714
|
+
* @param branch - Git branch (defaults to current)
|
|
715
|
+
* @param cwd - Working directory
|
|
716
|
+
* @returns Session and window names
|
|
717
|
+
* @throws Error if profile not found, validation fails, or non-prompt-scoped agent already exists
|
|
718
|
+
*/
|
|
719
|
+
export function spawnAgentFromProfile(
|
|
720
|
+
config: ProfileSpawnConfig,
|
|
721
|
+
branch?: string,
|
|
722
|
+
cwd?: string
|
|
723
|
+
): { sessionName: string; windowName: string } {
|
|
724
|
+
const profile = loadAgentProfile(config.agentName);
|
|
725
|
+
|
|
726
|
+
if (!profile) {
|
|
727
|
+
const available = listAgentProfiles();
|
|
728
|
+
throw new Error(
|
|
729
|
+
`Agent profile not found: ${config.agentName}. Available profiles: ${available.join(', ')}`
|
|
730
|
+
);
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
// Build the invocation (validates template vars)
|
|
734
|
+
const invocation = buildAgentInvocation(profile, config.context);
|
|
735
|
+
|
|
736
|
+
// Convert to SpawnConfig (flowOverride takes precedence over profile's default flow)
|
|
737
|
+
const spawnConfig: SpawnConfig = {
|
|
738
|
+
name: profile.name,
|
|
739
|
+
agentType: profile.name,
|
|
740
|
+
flowPath: config.flowOverride || invocation.flowPath,
|
|
741
|
+
preamble: invocation.preamble,
|
|
742
|
+
promptNumber: config.promptNumber,
|
|
743
|
+
specName: config.context.SPEC_NAME ?? undefined,
|
|
744
|
+
nonCoding: profile.nonCoding,
|
|
745
|
+
focusWindow: config.focusWindow,
|
|
746
|
+
promptScoped: profile.promptScoped,
|
|
747
|
+
};
|
|
748
|
+
|
|
749
|
+
return spawnAgent(spawnConfig, branch, cwd);
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
/**
|
|
753
|
+
* Configuration for custom flow spawning
|
|
754
|
+
*/
|
|
755
|
+
export interface CustomFlowConfig {
|
|
756
|
+
/** Absolute path to the flow file */
|
|
757
|
+
flowPath: string;
|
|
758
|
+
/** Custom message to use as system prompt/preamble */
|
|
759
|
+
customMessage: string;
|
|
760
|
+
/** Unique window name (e.g., "custom-flow-1") */
|
|
761
|
+
windowName: string;
|
|
762
|
+
/** If true, switch focus to the new window (default: true) */
|
|
763
|
+
focusWindow?: boolean;
|
|
764
|
+
/** Current spec name (optional, for context) */
|
|
765
|
+
specName?: string;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
/**
|
|
769
|
+
* Spawn a custom flow agent
|
|
770
|
+
*
|
|
771
|
+
* This allows running any flow file with a custom message as the preamble.
|
|
772
|
+
* The agent is tracked like profiled agents but without profile restrictions.
|
|
773
|
+
*
|
|
774
|
+
* @param config - Custom flow configuration
|
|
775
|
+
* @param branch - Git branch (defaults to current)
|
|
776
|
+
* @param cwd - Working directory
|
|
777
|
+
* @returns Session and window names
|
|
778
|
+
*/
|
|
779
|
+
export function spawnCustomFlow(
|
|
780
|
+
config: CustomFlowConfig,
|
|
781
|
+
branch?: string,
|
|
782
|
+
cwd?: string
|
|
783
|
+
): { sessionName: string; windowName: string } {
|
|
784
|
+
const currentBranch = branch || getCurrentBranch();
|
|
785
|
+
const sessionName = ensureSession(currentBranch, cwd);
|
|
786
|
+
const windowName = config.windowName;
|
|
787
|
+
const shouldFocus = config.focusWindow !== false;
|
|
788
|
+
|
|
789
|
+
// Kill existing window if present (allow respawning)
|
|
790
|
+
if (windowExists(sessionName, windowName)) {
|
|
791
|
+
killWindow(sessionName, windowName);
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
// Create new window (detached - don't switch focus yet)
|
|
795
|
+
createWindow(sessionName, windowName, cwd, true);
|
|
796
|
+
|
|
797
|
+
// Register this agent as spawned by ALL HANDS
|
|
798
|
+
registerSpawnedAgent(windowName);
|
|
799
|
+
|
|
800
|
+
// Build environment variables for the custom flow agent
|
|
801
|
+
// Note: BASE_BRANCH is communicated via the initial prompt, not env vars
|
|
802
|
+
const env: Record<string, string> = {
|
|
803
|
+
AGENT_ID: windowName,
|
|
804
|
+
AGENT_TYPE: 'custom-flow',
|
|
805
|
+
BRANCH: currentBranch,
|
|
806
|
+
};
|
|
807
|
+
|
|
808
|
+
if (config.specName) {
|
|
809
|
+
env.SPEC_NAME = config.specName;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
// Read the flow file content
|
|
813
|
+
let flowContent = '';
|
|
814
|
+
if (existsSync(config.flowPath)) {
|
|
815
|
+
flowContent = readFileSync(config.flowPath, 'utf-8');
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
// Write a launcher script to avoid all shell escaping issues
|
|
819
|
+
const tempDir = join(cwd || process.cwd(), '.allhands', 'harness', '.cache', 'launchers');
|
|
820
|
+
mkdirSync(tempDir, { recursive: true });
|
|
821
|
+
|
|
822
|
+
// Clean up old launcher files (older than 24h)
|
|
823
|
+
cleanupOldLaunchers(tempDir);
|
|
824
|
+
|
|
825
|
+
const launcherScript = join(tempDir, `${windowName}-launcher.sh`);
|
|
826
|
+
const promptFile = join(tempDir, `${windowName}-prompt.txt`);
|
|
827
|
+
|
|
828
|
+
// Build combined prompt: flow content + custom message + base branch info
|
|
829
|
+
// NO system prompt - everything goes into the initial user prompt
|
|
830
|
+
const baseBranch = getBaseBranch();
|
|
831
|
+
const promptParts: string[] = [];
|
|
832
|
+
|
|
833
|
+
if (flowContent) {
|
|
834
|
+
promptParts.push(flowContent);
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
if (config.customMessage && config.customMessage.trim()) {
|
|
838
|
+
promptParts.push(config.customMessage);
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
// Always append base branch info
|
|
842
|
+
promptParts.push(`The base branch name is "${baseBranch}".`);
|
|
843
|
+
|
|
844
|
+
const combinedPrompt = promptParts.join('\n\n');
|
|
845
|
+
writeFileSync(promptFile, combinedPrompt, 'utf-8');
|
|
846
|
+
|
|
847
|
+
// Build the launcher script
|
|
848
|
+
const scriptLines: string[] = ['#!/bin/bash', ''];
|
|
849
|
+
|
|
850
|
+
// Skip pyenv rehash to avoid lock contention when spawning multiple agents
|
|
851
|
+
scriptLines.push('export PYENV_REHASH_SKIP=1');
|
|
852
|
+
|
|
853
|
+
// Export environment variables
|
|
854
|
+
for (const [key, value] of Object.entries(env)) {
|
|
855
|
+
scriptLines.push(`export ${key}="${value}"`);
|
|
856
|
+
}
|
|
857
|
+
scriptLines.push('');
|
|
858
|
+
|
|
859
|
+
// Build claude command - NO system prompt, everything in initial prompt
|
|
860
|
+
const cmdParts: string[] = ['claude'];
|
|
861
|
+
cmdParts.push('--settings .claude/settings.json');
|
|
862
|
+
cmdParts.push('--dangerously-skip-permissions');
|
|
863
|
+
cmdParts.push(`"$(cat '${promptFile}')"`)
|
|
864
|
+
|
|
865
|
+
scriptLines.push(cmdParts.join(' \\\n '));
|
|
866
|
+
|
|
867
|
+
writeFileSync(launcherScript, scriptLines.join('\n'), { mode: 0o755 });
|
|
868
|
+
|
|
869
|
+
// Execute the launcher script with exec so it replaces the shell
|
|
870
|
+
sendKeys(sessionName, windowName, `exec bash '${launcherScript}'`);
|
|
871
|
+
|
|
872
|
+
// Switch focus to the new window if requested
|
|
873
|
+
if (shouldFocus) {
|
|
874
|
+
selectWindow(sessionName, windowName);
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
return { sessionName, windowName };
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
/**
|
|
881
|
+
* Build standard template context from planning state
|
|
882
|
+
*
|
|
883
|
+
* This constructs the context object needed for agent spawning
|
|
884
|
+
* by reading the current planning state.
|
|
885
|
+
*
|
|
886
|
+
* @param spec - The spec name (used for planning paths)
|
|
887
|
+
* @param specName - Optional display name for spec
|
|
888
|
+
* @param promptNumber - Optional prompt number
|
|
889
|
+
* @param promptPath - Optional prompt file path
|
|
890
|
+
* @param cwd - Working directory
|
|
891
|
+
*/
|
|
892
|
+
export function buildTemplateContext(
|
|
893
|
+
spec: string,
|
|
894
|
+
specName?: string,
|
|
895
|
+
promptNumber?: number,
|
|
896
|
+
promptPath?: string,
|
|
897
|
+
cwd?: string
|
|
898
|
+
): TemplateContext {
|
|
899
|
+
// Use spec for planning paths (directory key)
|
|
900
|
+
const paths = getPlanningPaths(spec, cwd);
|
|
901
|
+
const branch = getCurrentBranch(cwd);
|
|
902
|
+
|
|
903
|
+
// Resolve spec type for SPEC_TYPE template variable
|
|
904
|
+
const branchSpec = getSpecForBranch(branch, cwd);
|
|
905
|
+
|
|
906
|
+
const context: TemplateContext = {
|
|
907
|
+
BRANCH: branch,
|
|
908
|
+
PLANNING_FOLDER: paths.root,
|
|
909
|
+
PROMPTS_FOLDER: paths.prompts,
|
|
910
|
+
ALIGNMENT_PATH: paths.alignment,
|
|
911
|
+
OUTPUT_PATH: join(paths.root, 'e2e-test-plan.md'),
|
|
912
|
+
SPEC_TYPE: branchSpec?.type ?? 'milestone',
|
|
913
|
+
};
|
|
914
|
+
|
|
915
|
+
// Set spec name (use the display name if provided, else the directory name)
|
|
916
|
+
context.SPEC_NAME = specName || spec;
|
|
917
|
+
|
|
918
|
+
if (promptNumber !== undefined) {
|
|
919
|
+
context.PROMPT_NUMBER = String(promptNumber).padStart(2, '0');
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
if (promptPath) {
|
|
923
|
+
context.PROMPT_PATH = promptPath;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
// Add hypothesis domains from settings.json
|
|
927
|
+
const settings = loadProjectSettings();
|
|
928
|
+
const defaultDomains = ['testing', 'stability', 'performance', 'feature', 'ux', 'integration'];
|
|
929
|
+
const domains = settings?.emergent?.hypothesisDomains ?? defaultDomains;
|
|
930
|
+
context.HYPOTHESIS_DOMAINS = domains.join(', ');
|
|
931
|
+
|
|
932
|
+
// Try to read spec path from status (YAML format)
|
|
933
|
+
if (existsSync(paths.status)) {
|
|
934
|
+
try {
|
|
935
|
+
const content = readFileSync(paths.status, 'utf-8');
|
|
936
|
+
const specMatch = content.match(/^spec:\s*(.+)/m);
|
|
937
|
+
if (specMatch) {
|
|
938
|
+
context.SPEC_PATH = specMatch[1].trim();
|
|
939
|
+
}
|
|
940
|
+
} catch {
|
|
941
|
+
// Ignore parse errors
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
// Resolve WORKFLOW_DOMAIN_PATH from spec's initial_workflow_domain frontmatter
|
|
946
|
+
const basePath = cwd || process.cwd();
|
|
947
|
+
const workflowDomain = context.SPEC_PATH
|
|
948
|
+
? getWorkflowDomain(join(basePath, context.SPEC_PATH))
|
|
949
|
+
: 'milestone';
|
|
950
|
+
const workflowDomainPath = join(basePath, '.allhands', 'workflows', `${workflowDomain}.md`);
|
|
951
|
+
if (existsSync(workflowDomainPath)) {
|
|
952
|
+
context.WORKFLOW_DOMAIN_PATH = workflowDomainPath;
|
|
953
|
+
} else {
|
|
954
|
+
console.warn(`Workflow domain config not found: ${workflowDomainPath}`);
|
|
955
|
+
context.WORKFLOW_DOMAIN_PATH = '';
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
return context;
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
/**
|
|
962
|
+
* Attach to the tmux session
|
|
963
|
+
*/
|
|
964
|
+
export function attachSession(sessionName: string): void {
|
|
965
|
+
// This will take over the terminal
|
|
966
|
+
const child = spawn('tmux', ['attach-session', '-t', sessionName], {
|
|
967
|
+
stdio: 'inherit',
|
|
968
|
+
});
|
|
969
|
+
|
|
970
|
+
child.on('exit', () => {
|
|
971
|
+
process.exit(0);
|
|
972
|
+
});
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
/**
|
|
976
|
+
* Get info about all running agents
|
|
977
|
+
*
|
|
978
|
+
* Only returns agents that were spawned by ALL HANDS (tracked in registry)
|
|
979
|
+
* AND still exist in tmux.
|
|
980
|
+
*/
|
|
981
|
+
export function getRunningAgents(branch?: string): Array<{
|
|
982
|
+
windowName: string;
|
|
983
|
+
agentType?: string;
|
|
984
|
+
}> {
|
|
985
|
+
// Use current session if we're in tmux, otherwise fall back to named session
|
|
986
|
+
const currentSession = getCurrentSession();
|
|
987
|
+
const sessionName = currentSession || getSessionName(branch);
|
|
988
|
+
|
|
989
|
+
if (!sessionExists(sessionName)) {
|
|
990
|
+
return [];
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
const windows = listWindows(sessionName);
|
|
994
|
+
const registry = getSpawnedAgentRegistry();
|
|
995
|
+
|
|
996
|
+
// Filter to agent windows that:
|
|
997
|
+
// 1. Are in our spawned registry (were created by ALL HANDS)
|
|
998
|
+
// 2. Still exist in tmux
|
|
999
|
+
// 3. Are not the TUI/hub window
|
|
1000
|
+
return windows
|
|
1001
|
+
.filter((w) => w.index > 0 && w.name !== 'hub' && registry.has(w.name))
|
|
1002
|
+
.map((w) => ({
|
|
1003
|
+
windowName: w.name,
|
|
1004
|
+
agentType: inferAgentType(w.name),
|
|
1005
|
+
}));
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
/**
|
|
1009
|
+
* Get all valid agent types from profiles.
|
|
1010
|
+
*/
|
|
1011
|
+
export function getAgentTypes(): string[] {
|
|
1012
|
+
return listAgentProfiles().map((name) => {
|
|
1013
|
+
const profile = loadAgentProfile(name);
|
|
1014
|
+
return profile?.name ?? name;
|
|
1015
|
+
});
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
/**
|
|
1019
|
+
* Infer agent type from window name using agent profiles.
|
|
1020
|
+
*
|
|
1021
|
+
* Window names follow patterns:
|
|
1022
|
+
* - Non-prompt-scoped: exact profile name (e.g., "planner")
|
|
1023
|
+
* - Prompt-scoped: "{name}-{NN}" (e.g., "executor-01")
|
|
1024
|
+
*/
|
|
1025
|
+
function inferAgentType(windowName: string): AgentType | undefined {
|
|
1026
|
+
const lowerName = windowName.toLowerCase();
|
|
1027
|
+
|
|
1028
|
+
// Load all profiles and match against window name
|
|
1029
|
+
const profileNames = listAgentProfiles();
|
|
1030
|
+
|
|
1031
|
+
for (const profileName of profileNames) {
|
|
1032
|
+
const profile = loadAgentProfile(profileName);
|
|
1033
|
+
if (!profile) continue;
|
|
1034
|
+
|
|
1035
|
+
const name = profile.name.toLowerCase();
|
|
1036
|
+
|
|
1037
|
+
if (!profile.promptScoped) {
|
|
1038
|
+
// Non-prompt-scoped: exact match
|
|
1039
|
+
if (lowerName === name) {
|
|
1040
|
+
return name;
|
|
1041
|
+
}
|
|
1042
|
+
} else {
|
|
1043
|
+
// Prompt-scoped: match "{name}" or "{name}-{NN}"
|
|
1044
|
+
if (lowerName === name || lowerName.match(new RegExp(`^${name}-\\d+$`))) {
|
|
1045
|
+
return name;
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
return undefined;
|
|
1051
|
+
}
|