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.
Files changed (164) hide show
  1. {windcode-0.1.2 → windcode-0.1.3}/PKG-INFO +1 -1
  2. {windcode-0.1.2 → windcode-0.1.3}/pyproject.toml +1 -1
  3. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/__init__.py +1 -1
  4. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/domain/subagents.py +3 -1
  5. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/extensions/mcp/tools.py +7 -2
  6. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/runtime/prompts.py +4 -0
  7. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/runtime/subagents/coordinator.py +35 -5
  8. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/runtime/subagents/factory.py +7 -0
  9. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/runtime/subagents/roles.py +15 -6
  10. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/sdk.py +15 -1
  11. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/tools/subagents/spawn.py +24 -5
  12. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/worktrees/manager.py +54 -20
  13. {windcode-0.1.2 → windcode-0.1.3}/uv.lock +1 -1
  14. {windcode-0.1.2 → windcode-0.1.3}/.github/workflows/publish.yml +0 -0
  15. {windcode-0.1.2 → windcode-0.1.3}/.gitignore +0 -0
  16. {windcode-0.1.2 → windcode-0.1.3}/.python-version +0 -0
  17. {windcode-0.1.2 → windcode-0.1.3}/.windcode/config.toml.example +0 -0
  18. {windcode-0.1.2 → windcode-0.1.3}/.windcode/skills/code-review/SKILL.md +0 -0
  19. {windcode-0.1.2 → windcode-0.1.3}/.windcode/skills/code-review/agents/openai.yaml +0 -0
  20. {windcode-0.1.2 → windcode-0.1.3}/AGENTS.md +0 -0
  21. {windcode-0.1.2 → windcode-0.1.3}/LICENSE +0 -0
  22. {windcode-0.1.2 → windcode-0.1.3}/README.md +0 -0
  23. {windcode-0.1.2 → windcode-0.1.3}/assets/image.png +0 -0
  24. {windcode-0.1.2 → windcode-0.1.3}/assets/image1.png +0 -0
  25. {windcode-0.1.2 → windcode-0.1.3}/assets/image2.png +0 -0
  26. {windcode-0.1.2 → windcode-0.1.3}/assets/image3.png +0 -0
  27. {windcode-0.1.2 → windcode-0.1.3}/assets/image4.png +0 -0
  28. {windcode-0.1.2 → windcode-0.1.3}/examples/01_stateful_chat.py +0 -0
  29. {windcode-0.1.2 → windcode-0.1.3}/examples/02_custom_tool.py +0 -0
  30. {windcode-0.1.2 → windcode-0.1.3}/examples/03_multi_agent.py +0 -0
  31. {windcode-0.1.2 → windcode-0.1.3}/examples/04_mcp_tools.py +0 -0
  32. {windcode-0.1.2 → windcode-0.1.3}/examples/05_skill.py +0 -0
  33. {windcode-0.1.2 → windcode-0.1.3}/examples/06_stream_and_cancel.py +0 -0
  34. {windcode-0.1.2 → windcode-0.1.3}/examples/07_bash_approval.py +0 -0
  35. {windcode-0.1.2 → windcode-0.1.3}/examples/README.md +0 -0
  36. {windcode-0.1.2 → windcode-0.1.3}/examples/skills/release_notes/SKILL.md +0 -0
  37. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/__main__.py +0 -0
  38. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/auth/__init__.py +0 -0
  39. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/auth/store.py +0 -0
  40. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/cli.py +0 -0
  41. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/config/__init__.py +0 -0
  42. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/config/loader.py +0 -0
  43. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/config/models.py +0 -0
  44. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/config/writer.py +0 -0
  45. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/context/__init__.py +0 -0
  46. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/context/compactor.py +0 -0
  47. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/context/estimator.py +0 -0
  48. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/context/truncation.py +0 -0
  49. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/domain/__init__.py +0 -0
  50. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/domain/errors.py +0 -0
  51. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/domain/events.py +0 -0
  52. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/domain/messages.py +0 -0
  53. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/domain/models.py +0 -0
  54. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/domain/tools.py +0 -0
  55. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/extensions/__init__.py +0 -0
  56. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/extensions/commands.py +0 -0
  57. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/extensions/discovery.py +0 -0
  58. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/extensions/events.py +0 -0
  59. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/extensions/hooks/__init__.py +0 -0
  60. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/extensions/hooks/dispatcher.py +0 -0
  61. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/extensions/hooks/executor.py +0 -0
  62. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/extensions/hooks/loader.py +0 -0
  63. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/extensions/hooks/models.py +0 -0
  64. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/extensions/mcp/__init__.py +0 -0
  65. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/extensions/mcp/adapter.py +0 -0
  66. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/extensions/mcp/catalog.py +0 -0
  67. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/extensions/mcp/client.py +0 -0
  68. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/extensions/mcp/runtime.py +0 -0
  69. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/extensions/models.py +0 -0
  70. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/extensions/paths.py +0 -0
  71. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/extensions/plugins/__init__.py +0 -0
  72. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/extensions/plugins/installer.py +0 -0
  73. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/extensions/plugins/manifest.py +0 -0
  74. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/extensions/runtime.py +0 -0
  75. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/extensions/service.py +0 -0
  76. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/extensions/skills/__init__.py +0 -0
  77. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/extensions/skills/loader.py +0 -0
  78. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/extensions/skills/parser.py +0 -0
  79. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/extensions/skills/tools.py +0 -0
  80. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/extensions/snapshot.py +0 -0
  81. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/extensions/state.py +0 -0
  82. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/instructions/__init__.py +0 -0
  83. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/instructions/loader.py +0 -0
  84. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/memory/__init__.py +0 -0
  85. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/memory/extraction.py +0 -0
  86. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/memory/models.py +0 -0
  87. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/memory/refiner.py +0 -0
  88. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/memory/security.py +0 -0
  89. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/memory/service.py +0 -0
  90. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/memory/store.py +0 -0
  91. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/observability/__init__.py +0 -0
  92. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/observability/redaction.py +0 -0
  93. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/observability/trace.py +0 -0
  94. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/policy/__init__.py +0 -0
  95. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/policy/engine.py +0 -0
  96. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/policy/models.py +0 -0
  97. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/providers/__init__.py +0 -0
  98. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/providers/_utils.py +0 -0
  99. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/providers/anthropic.py +0 -0
  100. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/providers/base.py +0 -0
  101. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/providers/catalog.py +0 -0
  102. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/providers/errors.py +0 -0
  103. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/providers/openai_compat.py +0 -0
  104. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/providers/openai_responses.py +0 -0
  105. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/providers/registry.py +0 -0
  106. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/runtime/__init__.py +0 -0
  107. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/runtime/control.py +0 -0
  108. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/runtime/event_bus.py +0 -0
  109. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/runtime/loop.py +0 -0
  110. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/runtime/report.py +0 -0
  111. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/runtime/retry.py +0 -0
  112. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/runtime/scheduler.py +0 -0
  113. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/runtime/subagents/__init__.py +0 -0
  114. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/runtime/subagents/approvals.py +0 -0
  115. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/runtime/subagents/budgets.py +0 -0
  116. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/runtime/subagents/verification.py +0 -0
  117. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/sandbox/__init__.py +0 -0
  118. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/sandbox/bwrap.py +0 -0
  119. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/sessions/__init__.py +0 -0
  120. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/sessions/artifacts.py +0 -0
  121. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/sessions/models.py +0 -0
  122. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/sessions/store.py +0 -0
  123. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/sessions/tree.py +0 -0
  124. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/tools/__init__.py +0 -0
  125. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/tools/apply_patch.py +0 -0
  126. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/tools/ask_user.py +0 -0
  127. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/tools/builtins.py +0 -0
  128. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/tools/edit_file.py +0 -0
  129. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/tools/filesystem.py +0 -0
  130. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/tools/glob.py +0 -0
  131. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/tools/grep.py +0 -0
  132. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/tools/memory.py +0 -0
  133. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/tools/read_file.py +0 -0
  134. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/tools/registry.py +0 -0
  135. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/tools/shell.py +0 -0
  136. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/tools/subagents/__init__.py +0 -0
  137. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/tools/subagents/cancel.py +0 -0
  138. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/tools/subagents/integrate.py +0 -0
  139. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/tools/subagents/list.py +0 -0
  140. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/tools/subagents/wait.py +0 -0
  141. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/tools/write_file.py +0 -0
  142. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/tui/__init__.py +0 -0
  143. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/tui/app.py +0 -0
  144. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/tui/commands.py +0 -0
  145. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/tui/permission_display.py +0 -0
  146. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/tui/styles.tcss +0 -0
  147. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/tui/widgets/__init__.py +0 -0
  148. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/tui/widgets/approval.py +0 -0
  149. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/tui/widgets/command_menu.py +0 -0
  150. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/tui/widgets/extensions.py +0 -0
  151. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/tui/widgets/input.py +0 -0
  152. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/tui/widgets/memory.py +0 -0
  153. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/tui/widgets/messages.py +0 -0
  154. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/tui/widgets/models.py +0 -0
  155. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/tui/widgets/question.py +0 -0
  156. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/tui/widgets/sessions.py +0 -0
  157. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/tui/widgets/status.py +0 -0
  158. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/tui/widgets/subagents.py +0 -0
  159. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/tui/widgets/tools.py +0 -0
  160. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/tui/widgets/welcome.py +0 -0
  161. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/types.py +0 -0
  162. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/worktrees/__init__.py +0 -0
  163. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/worktrees/git.py +0 -0
  164. {windcode-0.1.2 → windcode-0.1.3}/src/windcode/worktrees/models.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: windcode
3
- Version: 0.1.2
3
+ Version: 0.1.3
4
4
  Summary: A safe, extensible terminal coding agent
5
5
  Project-URL: Homepage, https://github.com/tingfeng347/windcode
6
6
  Project-URL: Repository, https://github.com/tingfeng347/windcode
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "windcode"
3
- version = "0.1.2"
3
+ version = "0.1.3"
4
4
  description = "A safe, extensible terminal coding agent"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -3,4 +3,4 @@
3
3
  from windcode.sdk import Windcode
4
4
 
5
5
  __all__ = ["Windcode"]
6
- __version__ = "0.1.2"
6
+ __version__ = "0.1.3"
@@ -48,7 +48,9 @@ TERMINAL_SUBAGENT_STATUSES = frozenset(
48
48
  )
49
49
 
50
50
  _ALLOWED_TRANSITIONS: dict[SubagentStatus, frozenset[SubagentStatus]] = {
51
- SubagentStatus.QUEUED: frozenset({SubagentStatus.RUNNING, SubagentStatus.CANCELLED}),
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.registry = registry
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.registry.register(adapter, replace=True)
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(self.workspace)
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
- return tuple(created)
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(self.workspace)
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
- integration = await self.worktrees.integrate(self._leases[subagent_id], self.workspace)
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
- allowed_tools = policy.default_tools | network_tools
69
- if requested_tools is not None:
70
- unknown = requested_tools - allowed_tools
71
- if unknown:
72
- raise ValueError(f"requested tools exceed role policy: {', '.join(sorted(unknown))}")
73
- selected = allowed_tools if requested_tools is None else requested_tools
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(worktrees_root=self.state_root / "worktrees"),
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
- kind: SubagentTaskKind
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 = 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. Declare requires_network for network-dependent "
55
- "tasks; network access follows the parent run's network policy and permission workflow."
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
- async def validate_parent(self, workspace: Path) -> GitBaseline:
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
- status = (
72
- await self.runner.run(
73
- ("status", "--porcelain=v1", "--untracked-files=all"), cwd=repository
74
- )
75
- ).stdout
76
- if status.strip():
77
- details = ", ".join(line.strip() for line in status.splitlines()[:5])
78
- raise WorktreeError(
79
- GitErrorCategory.DIRTY_WORKSPACE,
80
- f"parent workspace is not clean: {details}",
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.worktrees_root or (
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
- if path.is_relative_to(baseline.repository):
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):
@@ -1498,7 +1498,7 @@ wheels = [
1498
1498
 
1499
1499
  [[package]]
1500
1500
  name = "windcode"
1501
- version = "0.1.2"
1501
+ version = "0.1.3"
1502
1502
  source = { editable = "." }
1503
1503
  dependencies = [
1504
1504
  { name = "aiohttp" },
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