windcode 0.1.2__tar.gz → 0.1.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {windcode-0.1.2 → windcode-0.1.3}/PKG-INFO +1 -1
- {windcode-0.1.2 → windcode-0.1.3}/pyproject.toml +1 -1
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/__init__.py +1 -1
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/domain/subagents.py +3 -1
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/extensions/mcp/tools.py +7 -2
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/runtime/prompts.py +4 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/runtime/subagents/coordinator.py +35 -5
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/runtime/subagents/factory.py +7 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/runtime/subagents/roles.py +15 -6
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/sdk.py +15 -1
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/tools/subagents/spawn.py +24 -5
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/worktrees/manager.py +54 -20
- {windcode-0.1.2 → windcode-0.1.3}/uv.lock +1 -1
- {windcode-0.1.2 → windcode-0.1.3}/.github/workflows/publish.yml +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/.gitignore +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/.python-version +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/.windcode/config.toml.example +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/.windcode/skills/code-review/SKILL.md +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/.windcode/skills/code-review/agents/openai.yaml +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/AGENTS.md +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/LICENSE +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/README.md +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/assets/image.png +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/assets/image1.png +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/assets/image2.png +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/assets/image3.png +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/assets/image4.png +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/examples/01_stateful_chat.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/examples/02_custom_tool.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/examples/03_multi_agent.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/examples/04_mcp_tools.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/examples/05_skill.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/examples/06_stream_and_cancel.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/examples/07_bash_approval.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/examples/README.md +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/examples/skills/release_notes/SKILL.md +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/__main__.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/auth/__init__.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/auth/store.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/cli.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/config/__init__.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/config/loader.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/config/models.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/config/writer.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/context/__init__.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/context/compactor.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/context/estimator.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/context/truncation.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/domain/__init__.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/domain/errors.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/domain/events.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/domain/messages.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/domain/models.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/domain/tools.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/extensions/__init__.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/extensions/commands.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/extensions/discovery.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/extensions/events.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/extensions/hooks/__init__.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/extensions/hooks/dispatcher.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/extensions/hooks/executor.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/extensions/hooks/loader.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/extensions/hooks/models.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/extensions/mcp/__init__.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/extensions/mcp/adapter.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/extensions/mcp/catalog.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/extensions/mcp/client.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/extensions/mcp/runtime.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/extensions/models.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/extensions/paths.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/extensions/plugins/__init__.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/extensions/plugins/installer.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/extensions/plugins/manifest.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/extensions/runtime.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/extensions/service.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/extensions/skills/__init__.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/extensions/skills/loader.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/extensions/skills/parser.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/extensions/skills/tools.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/extensions/snapshot.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/extensions/state.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/instructions/__init__.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/instructions/loader.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/memory/__init__.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/memory/extraction.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/memory/models.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/memory/refiner.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/memory/security.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/memory/service.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/memory/store.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/observability/__init__.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/observability/redaction.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/observability/trace.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/policy/__init__.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/policy/engine.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/policy/models.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/providers/__init__.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/providers/_utils.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/providers/anthropic.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/providers/base.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/providers/catalog.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/providers/errors.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/providers/openai_compat.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/providers/openai_responses.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/providers/registry.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/runtime/__init__.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/runtime/control.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/runtime/event_bus.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/runtime/loop.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/runtime/report.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/runtime/retry.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/runtime/scheduler.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/runtime/subagents/__init__.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/runtime/subagents/approvals.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/runtime/subagents/budgets.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/runtime/subagents/verification.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/sandbox/__init__.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/sandbox/bwrap.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/sessions/__init__.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/sessions/artifacts.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/sessions/models.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/sessions/store.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/sessions/tree.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/tools/__init__.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/tools/apply_patch.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/tools/ask_user.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/tools/builtins.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/tools/edit_file.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/tools/filesystem.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/tools/glob.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/tools/grep.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/tools/memory.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/tools/read_file.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/tools/registry.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/tools/shell.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/tools/subagents/__init__.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/tools/subagents/cancel.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/tools/subagents/integrate.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/tools/subagents/list.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/tools/subagents/wait.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/tools/write_file.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/tui/__init__.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/tui/app.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/tui/commands.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/tui/permission_display.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/tui/styles.tcss +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/tui/widgets/__init__.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/tui/widgets/approval.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/tui/widgets/command_menu.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/tui/widgets/extensions.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/tui/widgets/input.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/tui/widgets/memory.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/tui/widgets/messages.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/tui/widgets/models.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/tui/widgets/question.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/tui/widgets/sessions.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/tui/widgets/status.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/tui/widgets/subagents.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/tui/widgets/tools.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/tui/widgets/welcome.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/types.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/worktrees/__init__.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/worktrees/git.py +0 -0
- {windcode-0.1.2 → windcode-0.1.3}/src/windcode/worktrees/models.py +0 -0
|
@@ -48,7 +48,9 @@ TERMINAL_SUBAGENT_STATUSES = frozenset(
|
|
|
48
48
|
)
|
|
49
49
|
|
|
50
50
|
_ALLOWED_TRANSITIONS: dict[SubagentStatus, frozenset[SubagentStatus]] = {
|
|
51
|
-
SubagentStatus.QUEUED: frozenset(
|
|
51
|
+
SubagentStatus.QUEUED: frozenset(
|
|
52
|
+
{SubagentStatus.RUNNING, SubagentStatus.FAILED, SubagentStatus.CANCELLED}
|
|
53
|
+
),
|
|
52
54
|
SubagentStatus.RUNNING: frozenset(
|
|
53
55
|
{
|
|
54
56
|
SubagentStatus.BLOCKED,
|
|
@@ -439,13 +439,18 @@ class SearchMcpToolsTool:
|
|
|
439
439
|
selected_tools: set[str],
|
|
440
440
|
) -> None:
|
|
441
441
|
self.service = service
|
|
442
|
-
self.
|
|
442
|
+
self.registries = [registry]
|
|
443
443
|
self.selected_tools = selected_tools
|
|
444
444
|
|
|
445
|
+
def add_registry(self, registry: ToolRegistry) -> None:
|
|
446
|
+
if all(item is not registry for item in self.registries):
|
|
447
|
+
self.registries.append(registry)
|
|
448
|
+
|
|
445
449
|
async def _select(self, stable_id: str) -> tuple[McpToolAdapter, bool]:
|
|
446
450
|
already_selected = stable_id in self.selected_tools
|
|
447
451
|
adapter = await self.service.adapter(stable_id)
|
|
448
|
-
self.
|
|
452
|
+
for registry in self.registries:
|
|
453
|
+
registry.register(adapter, replace=True)
|
|
449
454
|
self.selected_tools.add(stable_id)
|
|
450
455
|
return adapter, already_selected
|
|
451
456
|
|
|
@@ -77,6 +77,8 @@ def build_system_prompt(
|
|
|
77
77
|
"\n\n## 委派策略: explicit\n"
|
|
78
78
|
"仅当用户明确要求委派、并行或使用子智能体时, 才可调用子智能体工具。"
|
|
79
79
|
"创建后调用 wait_subagents 一次等待结果; 禁止循环调用 list_subagents。"
|
|
80
|
+
"写任务使用基于当前 HEAD 的独立 Git Worktree; 不得为创建子智能体而暂存、提交、"
|
|
81
|
+
"还原、移动或删除父工作区的未提交修改。"
|
|
80
82
|
"子智能体可按运行网络策略和权限审批访问外部网络。"
|
|
81
83
|
)
|
|
82
84
|
elif delegation_mode is DelegationMode.PROACTIVE:
|
|
@@ -84,6 +86,8 @@ def build_system_prompt(
|
|
|
84
86
|
"\n\n## 委派策略: proactive\n"
|
|
85
87
|
"可在任务确实独立且适合并行时主动委派; 必须保持任务有界、状态可见并统一汇总。"
|
|
86
88
|
"创建后调用 wait_subagents 一次等待结果; 禁止循环调用 list_subagents。"
|
|
89
|
+
"写任务使用基于当前 HEAD 的独立 Git Worktree; 不得为创建子智能体而暂存、提交、"
|
|
90
|
+
"还原、移动或删除父工作区的未提交修改。"
|
|
87
91
|
"子智能体可按运行网络策略和权限审批访问外部网络。"
|
|
88
92
|
)
|
|
89
93
|
if memory_enabled:
|
|
@@ -46,7 +46,7 @@ from windcode.runtime.subagents.approvals import ApprovalRouter
|
|
|
46
46
|
from windcode.runtime.subagents.budgets import AggregateBudget
|
|
47
47
|
from windcode.runtime.subagents.factory import ChildRuntime, ChildRuntimeFactory
|
|
48
48
|
from windcode.runtime.subagents.verification import VerificationRunner
|
|
49
|
-
from windcode.worktrees import GitBaseline, WorktreeLease, WorktreeManager
|
|
49
|
+
from windcode.worktrees import GitBaseline, WorktreeError, WorktreeLease, WorktreeManager
|
|
50
50
|
|
|
51
51
|
|
|
52
52
|
class SubagentCoordinatorError(RuntimeError):
|
|
@@ -98,6 +98,7 @@ class SubagentCoordinator:
|
|
|
98
98
|
self._runtimes: dict[str, ChildRuntime] = {}
|
|
99
99
|
self._tasks: dict[str, asyncio.Task[None]] = {}
|
|
100
100
|
self._completion: dict[str, asyncio.Future[SubagentResult]] = {}
|
|
101
|
+
self._startup: dict[str, asyncio.Future[None]] = {}
|
|
101
102
|
self._usage: dict[str, Usage] = {}
|
|
102
103
|
self._queue: deque[str] = deque()
|
|
103
104
|
self._active = 0
|
|
@@ -168,7 +169,10 @@ class SubagentCoordinator:
|
|
|
168
169
|
baseline: GitBaseline | None = None
|
|
169
170
|
if any(spec.kind is SubagentTaskKind.WRITE for spec in specs):
|
|
170
171
|
try:
|
|
171
|
-
baseline = await self.worktrees.validate_parent(
|
|
172
|
+
baseline = await self.worktrees.validate_parent(
|
|
173
|
+
self.workspace,
|
|
174
|
+
require_clean=False,
|
|
175
|
+
)
|
|
172
176
|
except Exception as exc:
|
|
173
177
|
raise SubagentCoordinatorError("write_workspace_blocked", str(exc)) from exc
|
|
174
178
|
|
|
@@ -190,12 +194,22 @@ class SubagentCoordinator:
|
|
|
190
194
|
)
|
|
191
195
|
self._records[record.subagent_id] = record
|
|
192
196
|
self._completion[record.subagent_id] = loop.create_future()
|
|
197
|
+
self._startup[record.subagent_id] = loop.create_future()
|
|
193
198
|
self._queue.append(record.subagent_id)
|
|
194
199
|
await self._persist(record)
|
|
195
200
|
await self._publish_record_event(record, SubagentQueued)
|
|
196
201
|
created.append(record)
|
|
197
202
|
self._schedule_locked()
|
|
198
|
-
|
|
203
|
+
scheduled_ids = tuple(
|
|
204
|
+
record.subagent_id for record in created if record.subagent_id in self._tasks
|
|
205
|
+
)
|
|
206
|
+
# Do not report a successful spawn while every child is merely a queued
|
|
207
|
+
# record. Yield outside the coordinator lock until each scheduled task has
|
|
208
|
+
# either published its running state or reached a terminal failure.
|
|
209
|
+
await asyncio.gather(
|
|
210
|
+
*(asyncio.shield(self._startup[subagent_id]) for subagent_id in scheduled_ids)
|
|
211
|
+
)
|
|
212
|
+
return tuple(self._records[record.subagent_id] for record in created)
|
|
199
213
|
|
|
200
214
|
def _schedule_locked(self) -> None:
|
|
201
215
|
while self._queue and self._active < self.config.max_concurrent:
|
|
@@ -309,7 +323,10 @@ class SubagentCoordinator:
|
|
|
309
323
|
"",
|
|
310
324
|
record.base_commit,
|
|
311
325
|
)
|
|
312
|
-
validated = await self.worktrees.validate_parent(
|
|
326
|
+
validated = await self.worktrees.validate_parent(
|
|
327
|
+
self.workspace,
|
|
328
|
+
require_clean=False,
|
|
329
|
+
)
|
|
313
330
|
baseline = replace(
|
|
314
331
|
baseline, repository=validated.repository, branch=validated.branch
|
|
315
332
|
)
|
|
@@ -339,6 +356,9 @@ class SubagentCoordinator:
|
|
|
339
356
|
summary="subagent started",
|
|
340
357
|
workspace=str(workspace),
|
|
341
358
|
)
|
|
359
|
+
startup = self._startup[subagent_id]
|
|
360
|
+
if not startup.done():
|
|
361
|
+
startup.set_result(None)
|
|
342
362
|
forwarding = asyncio.create_task(self._forward_child_events(runtime))
|
|
343
363
|
try:
|
|
344
364
|
run_result = await runtime.loop.run(runtime.prompt, runtime.workspace)
|
|
@@ -429,6 +449,9 @@ class SubagentCoordinator:
|
|
|
429
449
|
if record.status in {SubagentStatus.QUEUED, SubagentStatus.RUNNING}:
|
|
430
450
|
await self._finish_failed(record, type(exc).__name__, str(exc))
|
|
431
451
|
finally:
|
|
452
|
+
startup = self._startup.get(subagent_id)
|
|
453
|
+
if startup is not None and not startup.done():
|
|
454
|
+
startup.set_result(None)
|
|
432
455
|
self._runtimes.pop(subagent_id, None)
|
|
433
456
|
self.approvals.cancel(subagent_id)
|
|
434
457
|
async with self._lock:
|
|
@@ -566,7 +589,14 @@ class SubagentCoordinator:
|
|
|
566
589
|
raise SubagentCoordinatorError(
|
|
567
590
|
"not_integratable", "subagent must be a completed clean write task"
|
|
568
591
|
)
|
|
569
|
-
|
|
592
|
+
try:
|
|
593
|
+
integration = await self.worktrees.integrate(self._leases[subagent_id], self.workspace)
|
|
594
|
+
except WorktreeError as exc:
|
|
595
|
+
raise SubagentCoordinatorError(
|
|
596
|
+
"integration_workspace_blocked",
|
|
597
|
+
"subagent work is safely committed in its Worktree, but integration is blocked: "
|
|
598
|
+
f"{exc}",
|
|
599
|
+
) from exc
|
|
570
600
|
if not integration.integrated:
|
|
571
601
|
conflict = await self._transition(record, SubagentStatus.CONFLICT)
|
|
572
602
|
result = replace(
|
|
@@ -135,12 +135,19 @@ class ChildRuntime:
|
|
|
135
135
|
def build_child_prompt(record: SubagentRecord) -> str:
|
|
136
136
|
spec = record.spec
|
|
137
137
|
verification = "\n".join(f"- {item}" for item in spec.verification)
|
|
138
|
+
delivery = (
|
|
139
|
+
"This is a read-only task. Return all findings in your final response for the parent "
|
|
140
|
+
"agent to consume. Do not create, edit, or save a report file, including through shell."
|
|
141
|
+
if spec.kind is SubagentTaskKind.READ
|
|
142
|
+
else "Complete file changes in the assigned worktree and commit them before responding."
|
|
143
|
+
)
|
|
138
144
|
return (
|
|
139
145
|
f"Task: {spec.task_name}\n"
|
|
140
146
|
f"Goal: {spec.goal}\n\n"
|
|
141
147
|
f"Context:\n{spec.context}\n\n"
|
|
142
148
|
f"Expected output:\n{spec.expected_output}\n\n"
|
|
143
149
|
f"Verification requirements:\n{verification}\n\n"
|
|
150
|
+
f"Delivery constraint:\n{delivery}\n\n"
|
|
144
151
|
"Complete only this task. Do not delegate and do not ask the user questions."
|
|
145
152
|
)
|
|
146
153
|
|
|
@@ -65,10 +65,19 @@ def resolve_role_tools(
|
|
|
65
65
|
if "__" in name
|
|
66
66
|
or name in {"list_mcp_servers", "search_mcp_tools", "read_mcp_resource", "get_mcp_prompt"}
|
|
67
67
|
)
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
68
|
+
# Task kind is an independent capability boundary. In particular, a worker
|
|
69
|
+
# may perform either kind of task, but a worker/read task must remain just as
|
|
70
|
+
# read-only as researcher/read and verifier/read.
|
|
71
|
+
kind_tools = (
|
|
72
|
+
policy.default_tools & _READ_TOOLS
|
|
73
|
+
if kind is SubagentTaskKind.READ
|
|
74
|
+
else policy.default_tools
|
|
75
|
+
)
|
|
76
|
+
allowed_tools = kind_tools | network_tools
|
|
77
|
+
# ``requested_tools`` is model-authored input, so treat it as a narrowing
|
|
78
|
+
# hint rather than a second policy boundary. A model can accidentally copy a
|
|
79
|
+
# write tool into a read-only researcher task; denying that tool is enough to
|
|
80
|
+
# preserve the role boundary and should not prevent the remaining research
|
|
81
|
+
# tools from starting.
|
|
82
|
+
selected = allowed_tools if requested_tools is None else requested_tools & allowed_tools
|
|
74
83
|
return selected & parent_tools
|
|
@@ -34,6 +34,7 @@ from windcode.extensions.commands import CommandRoute
|
|
|
34
34
|
from windcode.extensions.hooks.models import HookContext, HookEvent
|
|
35
35
|
from windcode.extensions.mcp.catalog import McpToolDefinition
|
|
36
36
|
from windcode.extensions.mcp.tools import (
|
|
37
|
+
SearchMcpToolsTool,
|
|
37
38
|
register_mcp_management_tools,
|
|
38
39
|
register_mcp_status_tool,
|
|
39
40
|
)
|
|
@@ -239,6 +240,12 @@ class Windcode:
|
|
|
239
240
|
project_root = self.workspace / project_root
|
|
240
241
|
return project_root.resolve()
|
|
241
242
|
|
|
243
|
+
def _user_storage_root(self) -> Path:
|
|
244
|
+
configured = self.config.storage.user_storage_root
|
|
245
|
+
if configured is not None:
|
|
246
|
+
return self._configured_state_path(configured)
|
|
247
|
+
return user_state_path("windcode").expanduser().resolve()
|
|
248
|
+
|
|
242
249
|
@classmethod
|
|
243
250
|
def open(
|
|
244
251
|
cls,
|
|
@@ -712,6 +719,10 @@ class Windcode:
|
|
|
712
719
|
continue
|
|
713
720
|
policy.restore_session_approval(tool_name, effects)
|
|
714
721
|
child_tools = run_registry.clone()
|
|
722
|
+
if "search_mcp_tools" in run_registry.names():
|
|
723
|
+
search_mcp_tools = run_registry.get("search_mcp_tools")
|
|
724
|
+
if isinstance(search_mcp_tools, SearchMcpToolsTool):
|
|
725
|
+
search_mcp_tools.add_registry(child_tools)
|
|
715
726
|
instructions = load_instructions(workspace, workspace_root=workspace)
|
|
716
727
|
run_memory = (
|
|
717
728
|
MemoryService(self.state_root, workspace) if self.config.memory.enabled else None
|
|
@@ -793,7 +804,10 @@ class Windcode:
|
|
|
793
804
|
config=self.config.subagents,
|
|
794
805
|
event_bus=bus,
|
|
795
806
|
factory=factory,
|
|
796
|
-
worktrees=WorktreeManager(
|
|
807
|
+
worktrees=WorktreeManager(
|
|
808
|
+
worktrees_root=self.state_root / "worktrees",
|
|
809
|
+
fallback_worktrees_root=self._user_storage_root() / "worktrees",
|
|
810
|
+
),
|
|
797
811
|
verification=VerificationRunner(
|
|
798
812
|
sandbox=sandbox,
|
|
799
813
|
timeout_seconds=self.config.budgets.shell_timeout_seconds,
|
|
@@ -14,13 +14,26 @@ class SubagentTaskInput(BaseModel):
|
|
|
14
14
|
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
15
15
|
|
|
16
16
|
task_name: str = Field(pattern=r"^[a-z0-9]+(?:_[a-z0-9]+)*$")
|
|
17
|
-
role: SubagentRole
|
|
18
|
-
|
|
17
|
+
role: SubagentRole = Field(
|
|
18
|
+
description=(
|
|
19
|
+
"researcher and verifier are read-only; use worker only when the child itself must "
|
|
20
|
+
"modify files"
|
|
21
|
+
)
|
|
22
|
+
)
|
|
23
|
+
kind: SubagentTaskKind = Field(
|
|
24
|
+
description="Use read for research/verification and write only for a worker editing files."
|
|
25
|
+
)
|
|
19
26
|
goal: str = Field(min_length=1)
|
|
20
27
|
context: str = Field(min_length=1)
|
|
21
28
|
expected_output: str = Field(min_length=1)
|
|
22
29
|
verification: tuple[str, ...] = Field(min_length=1)
|
|
23
|
-
allowed_tools: frozenset[str] | None =
|
|
30
|
+
allowed_tools: frozenset[str] | None = Field(
|
|
31
|
+
default=None,
|
|
32
|
+
description=(
|
|
33
|
+
"Optional tool restriction. Omit it to use role defaults. Never request write_file, "
|
|
34
|
+
"edit_file, or apply_patch for researcher/verifier read tasks."
|
|
35
|
+
),
|
|
36
|
+
)
|
|
24
37
|
model: str | None = None
|
|
25
38
|
requires_network: bool = Field(
|
|
26
39
|
default=False,
|
|
@@ -51,8 +64,14 @@ class SpawnSubagentsInput(BaseModel):
|
|
|
51
64
|
class SpawnSubagentsTool:
|
|
52
65
|
name = "spawn_subagents"
|
|
53
66
|
description = (
|
|
54
|
-
"Create bounded temporary subagents.
|
|
55
|
-
"
|
|
67
|
+
"Create bounded temporary subagents. Researchers and verifiers are read-only and return "
|
|
68
|
+
"their findings to the parent; use a worker/write task only when that child must edit "
|
|
69
|
+
"files. Write tasks use an isolated Git Worktree based on the current parent HEAD, so "
|
|
70
|
+
"never stash, commit, revert, move, or discard parent workspace changes before spawning; "
|
|
71
|
+
"uncommitted parent changes are intentionally not copied into the child Worktree. Omit "
|
|
72
|
+
"allowed_tools unless a narrower tool set is required. Declare "
|
|
73
|
+
"requires_network for network-dependent tasks; network access follows the parent run's "
|
|
74
|
+
"network policy and permission workflow."
|
|
56
75
|
)
|
|
57
76
|
input_model = SpawnSubagentsInput
|
|
58
77
|
effects = frozenset({ToolEffect.PROCESS, ToolEffect.WORKSPACE_WRITE})
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
|
+
import hashlib
|
|
4
5
|
import re
|
|
5
6
|
from pathlib import Path
|
|
6
7
|
|
|
@@ -42,12 +43,50 @@ class WorktreeManager:
|
|
|
42
43
|
*,
|
|
43
44
|
runner: GitRunner | None = None,
|
|
44
45
|
worktrees_root: Path | None = None,
|
|
46
|
+
fallback_worktrees_root: Path | None = None,
|
|
45
47
|
) -> None:
|
|
46
48
|
self.runner = runner or GitRunner()
|
|
47
49
|
self.worktrees_root = _resolve(worktrees_root) if worktrees_root else None
|
|
50
|
+
self.fallback_worktrees_root = (
|
|
51
|
+
_resolve(fallback_worktrees_root) if fallback_worktrees_root else None
|
|
52
|
+
)
|
|
48
53
|
self._lock = asyncio.Lock()
|
|
49
54
|
|
|
50
|
-
|
|
55
|
+
def _effective_root(self, repository: Path) -> Path:
|
|
56
|
+
default = repository.parent / f".{repository.name}-windcode-worktrees"
|
|
57
|
+
primary = self.worktrees_root
|
|
58
|
+
fallback = self.fallback_worktrees_root
|
|
59
|
+
if primary is not None and fallback is None:
|
|
60
|
+
return primary
|
|
61
|
+
if primary is not None and primary != fallback and primary.is_dir():
|
|
62
|
+
return primary
|
|
63
|
+
if fallback is not None:
|
|
64
|
+
digest = hashlib.sha256(str(repository).encode()).hexdigest()[:12]
|
|
65
|
+
project = _safe_component(repository.name)
|
|
66
|
+
return fallback / f"{project}-{digest}"
|
|
67
|
+
return default
|
|
68
|
+
|
|
69
|
+
async def _validate_nested_root(self, root: Path, repository: Path) -> None:
|
|
70
|
+
if not root.is_relative_to(repository):
|
|
71
|
+
return
|
|
72
|
+
probe = root / ".windcode-ignore-probe"
|
|
73
|
+
ignored = await self.runner.run(
|
|
74
|
+
("check-ignore", "--quiet", "--no-index", str(probe)),
|
|
75
|
+
cwd=repository,
|
|
76
|
+
check=False,
|
|
77
|
+
)
|
|
78
|
+
if ignored.returncode != 0:
|
|
79
|
+
raise WorktreeError(
|
|
80
|
+
GitErrorCategory.INVALID_PATH,
|
|
81
|
+
"a nested subagent Worktree root must be ignored by the parent repository",
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
async def validate_parent(
|
|
85
|
+
self,
|
|
86
|
+
workspace: Path,
|
|
87
|
+
*,
|
|
88
|
+
require_clean: bool = True,
|
|
89
|
+
) -> GitBaseline:
|
|
51
90
|
workspace = _resolve(workspace)
|
|
52
91
|
try:
|
|
53
92
|
repository_result = await self.runner.run(
|
|
@@ -68,17 +107,18 @@ class WorktreeManager:
|
|
|
68
107
|
)
|
|
69
108
|
commit = (await self.runner.run(("rev-parse", "HEAD"), cwd=repository)).stdout.strip()
|
|
70
109
|
await self.runner.run(("worktree", "list", "--porcelain"), cwd=repository)
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
(
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
110
|
+
if require_clean:
|
|
111
|
+
status = (
|
|
112
|
+
await self.runner.run(
|
|
113
|
+
("status", "--porcelain=v1", "--untracked-files=all"), cwd=repository
|
|
114
|
+
)
|
|
115
|
+
).stdout
|
|
116
|
+
if status.strip():
|
|
117
|
+
details = ", ".join(line.strip() for line in status.splitlines()[:5])
|
|
118
|
+
raise WorktreeError(
|
|
119
|
+
GitErrorCategory.DIRTY_WORKSPACE,
|
|
120
|
+
f"parent workspace is not clean: {details}",
|
|
121
|
+
)
|
|
82
122
|
return GitBaseline(
|
|
83
123
|
repository=repository, branch=branch_result.stdout.strip(), commit=commit
|
|
84
124
|
)
|
|
@@ -95,15 +135,9 @@ class WorktreeManager:
|
|
|
95
135
|
safe_task = _safe_component(task_name)
|
|
96
136
|
safe_id = _safe_component(subagent_id)
|
|
97
137
|
branch = f"windcode/subagents/{safe_run}/{safe_task}-{safe_id[:12]}"
|
|
98
|
-
root = self.
|
|
99
|
-
baseline.repository.parent / f".{baseline.repository.name}-windcode-worktrees"
|
|
100
|
-
)
|
|
138
|
+
root = self._effective_root(baseline.repository)
|
|
101
139
|
path = _resolve(root / f"{safe_task}-{safe_id[:12]}")
|
|
102
|
-
|
|
103
|
-
raise WorktreeError(
|
|
104
|
-
GitErrorCategory.INVALID_PATH,
|
|
105
|
-
"subagent Worktree must be outside the parent repository",
|
|
106
|
-
)
|
|
140
|
+
await self._validate_nested_root(root, baseline.repository)
|
|
107
141
|
async with self._lock:
|
|
108
142
|
_mkdir(root)
|
|
109
143
|
if _exists(path):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|