nonoka-cli 0.2.5__tar.gz → 0.2.6__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 (120) hide show
  1. {nonoka_cli-0.2.5 → nonoka_cli-0.2.6}/.gitignore +4 -1
  2. {nonoka_cli-0.2.5 → nonoka_cli-0.2.6}/PKG-INFO +1 -1
  3. {nonoka_cli-0.2.5 → nonoka_cli-0.2.6}/pyproject.toml +1 -1
  4. {nonoka_cli-0.2.5 → nonoka_cli-0.2.6}/src/nonoka_cli/bridge/server.py +1 -3
  5. {nonoka_cli-0.2.5 → nonoka_cli-0.2.6}/src/nonoka_cli/commands/opencode_cmd.py +3 -2
  6. {nonoka_cli-0.2.5 → nonoka_cli-0.2.6}/src/nonoka_cli/config/models.py +3 -9
  7. {nonoka_cli-0.2.5 → nonoka_cli-0.2.6}/src/nonoka_cli/core/agent_factory.py +6 -22
  8. {nonoka_cli-0.2.5 → nonoka_cli-0.2.6}/src/nonoka_cli/core/orchestrator.py +1 -26
  9. nonoka_cli-0.2.6/src/nonoka_cli/core/planning_service.py +89 -0
  10. {nonoka_cli-0.2.5 → nonoka_cli-0.2.6}/src/nonoka_cli/core/prompt_builder.py +4 -13
  11. {nonoka_cli-0.2.5 → nonoka_cli-0.2.6}/src/nonoka_cli/tools/builtins/agent_tools.py +3 -3
  12. {nonoka_cli-0.2.5 → nonoka_cli-0.2.6}/tests/unit/commands/test_opencode_cmd.py +0 -25
  13. {nonoka_cli-0.2.5 → nonoka_cli-0.2.6}/tests/unit/config/test_models.py +0 -27
  14. {nonoka_cli-0.2.5 → nonoka_cli-0.2.6}/tests/unit/core/test_agent_factory.py +0 -46
  15. nonoka_cli-0.2.6/tests/unit/core/test_orchestrator.py +98 -0
  16. nonoka_cli-0.2.6/tests/unit/core/test_planning_service.py +40 -0
  17. nonoka_cli-0.2.5/.agents/skills/nonoka-tui-testing/SKILL.md +0 -413
  18. nonoka_cli-0.2.5/.agents/skills/nonoka-tui-testing/references/checklist.md +0 -48
  19. nonoka_cli-0.2.5/.agents/skills/nonoka-tui-testing/scripts/analyze-timeline.py +0 -100
  20. nonoka_cli-0.2.5/.agents/skills/nonoka-tui-testing/scripts/capture-screenshots.py +0 -191
  21. nonoka_cli-0.2.5/.agents/skills/nonoka-tui-testing/scripts/setup-sandbox.sh +0 -237
  22. nonoka_cli-0.2.5/.agents/skills/tui-testing/SKILL.md +0 -60
  23. nonoka_cli-0.2.5/.agents/skills/tui-testing/scripts/opencode_tui.py +0 -288
  24. nonoka_cli-0.2.5/.nonoka/repo_map.jsonl +0 -85
  25. nonoka_cli-0.2.5/docs/external-mcp-skill-design.md +0 -542
  26. nonoka_cli-0.2.5/docs/nonoka-cli-complex-task-evaluation.md +0 -254
  27. nonoka_cli-0.2.5/docs/nonoka-cli-refactor-design.md +0 -735
  28. nonoka_cli-0.2.5/docs/subagent-refactor-design.md +0 -162
  29. nonoka_cli-0.2.5/docs/subagent-refactor-plan.md +0 -134
  30. nonoka_cli-0.2.5/src/nonoka_cli/core/planning_service.py +0 -192
  31. nonoka_cli-0.2.5/src/nonoka_cli/core/review_service.py +0 -293
  32. nonoka_cli-0.2.5/src/nonoka_cli/core/subagent_service.py +0 -184
  33. nonoka_cli-0.2.5/tests/unit/core/test_orchestrator.py +0 -205
  34. nonoka_cli-0.2.5/tests/unit/core/test_planning_service.py +0 -174
  35. nonoka_cli-0.2.5/tests/unit/core/test_review_service.py +0 -251
  36. {nonoka_cli-0.2.5 → nonoka_cli-0.2.6}/.nonoka/plugin.json.example +0 -0
  37. {nonoka_cli-0.2.5 → nonoka_cli-0.2.6}/LICENSE +0 -0
  38. {nonoka_cli-0.2.5 → nonoka_cli-0.2.6}/NOTICE +0 -0
  39. {nonoka_cli-0.2.5 → nonoka_cli-0.2.6}/README.md +0 -0
  40. {nonoka_cli-0.2.5 → nonoka_cli-0.2.6}/TODO.md +0 -0
  41. {nonoka_cli-0.2.5 → nonoka_cli-0.2.6}/install.sh +0 -0
  42. {nonoka_cli-0.2.5 → nonoka_cli-0.2.6}/nonoka.yaml +0 -0
  43. {nonoka_cli-0.2.5 → nonoka_cli-0.2.6}/nonoka.yaml.example +0 -0
  44. {nonoka_cli-0.2.5 → nonoka_cli-0.2.6}/packages/nonoka-opencode-provider/.gitignore +0 -0
  45. {nonoka_cli-0.2.5 → nonoka_cli-0.2.6}/packages/nonoka-opencode-provider/LICENSE +0 -0
  46. {nonoka_cli-0.2.5 → nonoka_cli-0.2.6}/packages/nonoka-opencode-provider/README.md +0 -0
  47. {nonoka_cli-0.2.5 → nonoka_cli-0.2.6}/packages/nonoka-opencode-provider/bun.lock +0 -0
  48. {nonoka_cli-0.2.5 → nonoka_cli-0.2.6}/packages/nonoka-opencode-provider/package.json +0 -0
  49. {nonoka_cli-0.2.5 → nonoka_cli-0.2.6}/packages/nonoka-opencode-provider/src/index.ts +0 -0
  50. {nonoka_cli-0.2.5 → nonoka_cli-0.2.6}/packages/nonoka-opencode-provider/src/nonoka-language-model.ts +0 -0
  51. {nonoka_cli-0.2.5 → nonoka_cli-0.2.6}/packages/nonoka-opencode-provider/src/protocol.ts +0 -0
  52. {nonoka_cli-0.2.5 → nonoka_cli-0.2.6}/packages/nonoka-opencode-provider/src/stream.ts +0 -0
  53. {nonoka_cli-0.2.5 → nonoka_cli-0.2.6}/packages/nonoka-opencode-provider/tests/language-model.test.ts +0 -0
  54. {nonoka_cli-0.2.5 → nonoka_cli-0.2.6}/packages/nonoka-opencode-provider/tests/protocol.test.ts +0 -0
  55. {nonoka_cli-0.2.5 → nonoka_cli-0.2.6}/packages/nonoka-opencode-provider/tests/stream.test.ts +0 -0
  56. {nonoka_cli-0.2.5 → nonoka_cli-0.2.6}/packages/nonoka-opencode-provider/tsconfig.json +0 -0
  57. {nonoka_cli-0.2.5 → nonoka_cli-0.2.6}/scripts/quick-install.sh +0 -0
  58. {nonoka_cli-0.2.5 → nonoka_cli-0.2.6}/src/nonoka_cli/__init__.py +0 -0
  59. {nonoka_cli-0.2.5 → nonoka_cli-0.2.6}/src/nonoka_cli/__main__.py +0 -0
  60. {nonoka_cli-0.2.5 → nonoka_cli-0.2.6}/src/nonoka_cli/bridge/__init__.py +0 -0
  61. {nonoka_cli-0.2.5 → nonoka_cli-0.2.6}/src/nonoka_cli/bridge/events.py +0 -0
  62. {nonoka_cli-0.2.5 → nonoka_cli-0.2.6}/src/nonoka_cli/bridge/handler.py +0 -0
  63. {nonoka_cli-0.2.5 → nonoka_cli-0.2.6}/src/nonoka_cli/bridge/protocol.py +0 -0
  64. {nonoka_cli-0.2.5 → nonoka_cli-0.2.6}/src/nonoka_cli/cli.py +0 -0
  65. {nonoka_cli-0.2.5 → nonoka_cli-0.2.6}/src/nonoka_cli/commands/__init__.py +0 -0
  66. {nonoka_cli-0.2.5 → nonoka_cli-0.2.6}/src/nonoka_cli/commands/config_cmd.py +0 -0
  67. {nonoka_cli-0.2.5 → nonoka_cli-0.2.6}/src/nonoka_cli/commands/doctor_cmd.py +0 -0
  68. {nonoka_cli-0.2.5 → nonoka_cli-0.2.6}/src/nonoka_cli/commands/plugin_cmd.py +0 -0
  69. {nonoka_cli-0.2.5 → nonoka_cli-0.2.6}/src/nonoka_cli/commands/run_cmd.py +0 -0
  70. {nonoka_cli-0.2.5 → nonoka_cli-0.2.6}/src/nonoka_cli/config/__init__.py +0 -0
  71. {nonoka_cli-0.2.5 → nonoka_cli-0.2.6}/src/nonoka_cli/config/loader.py +0 -0
  72. {nonoka_cli-0.2.5 → nonoka_cli-0.2.6}/src/nonoka_cli/config/manager.py +0 -0
  73. {nonoka_cli-0.2.5 → nonoka_cli-0.2.6}/src/nonoka_cli/core/__init__.py +0 -0
  74. {nonoka_cli-0.2.5 → nonoka_cli-0.2.6}/src/nonoka_cli/core/context.py +0 -0
  75. {nonoka_cli-0.2.5 → nonoka_cli-0.2.6}/src/nonoka_cli/core/context_trimmer.py +0 -0
  76. {nonoka_cli-0.2.5 → nonoka_cli-0.2.6}/src/nonoka_cli/core/git_service.py +0 -0
  77. {nonoka_cli-0.2.5 → nonoka_cli-0.2.6}/src/nonoka_cli/core/mcp_service.py +0 -0
  78. {nonoka_cli-0.2.5 → nonoka_cli-0.2.6}/src/nonoka_cli/core/namespaces.py +0 -0
  79. {nonoka_cli-0.2.5 → nonoka_cli-0.2.6}/src/nonoka_cli/core/plugin_manifest.py +0 -0
  80. {nonoka_cli-0.2.5 → nonoka_cli-0.2.6}/src/nonoka_cli/core/plugin_manifest_converter.py +0 -0
  81. {nonoka_cli-0.2.5 → nonoka_cli-0.2.6}/src/nonoka_cli/core/repo_map_service.py +0 -0
  82. {nonoka_cli-0.2.5 → nonoka_cli-0.2.6}/src/nonoka_cli/core/runner_service.py +0 -0
  83. {nonoka_cli-0.2.5 → nonoka_cli-0.2.6}/src/nonoka_cli/core/session_service.py +0 -0
  84. {nonoka_cli-0.2.5 → nonoka_cli-0.2.6}/src/nonoka_cli/core/task_state.py +0 -0
  85. {nonoka_cli-0.2.5 → nonoka_cli-0.2.6}/src/nonoka_cli/core/tool_output_policy.py +0 -0
  86. {nonoka_cli-0.2.5 → nonoka_cli-0.2.6}/src/nonoka_cli/core/tool_service.py +0 -0
  87. {nonoka_cli-0.2.5 → nonoka_cli-0.2.6}/src/nonoka_cli/mcp/__init__.py +0 -0
  88. {nonoka_cli-0.2.5 → nonoka_cli-0.2.6}/src/nonoka_cli/mcp/manager.py +0 -0
  89. {nonoka_cli-0.2.5 → nonoka_cli-0.2.6}/src/nonoka_cli/mcp/models.py +0 -0
  90. {nonoka_cli-0.2.5 → nonoka_cli-0.2.6}/src/nonoka_cli/sessions/__init__.py +0 -0
  91. {nonoka_cli-0.2.5 → nonoka_cli-0.2.6}/src/nonoka_cli/sessions/manager.py +0 -0
  92. {nonoka_cli-0.2.5 → nonoka_cli-0.2.6}/src/nonoka_cli/sessions/models.py +0 -0
  93. {nonoka_cli-0.2.5 → nonoka_cli-0.2.6}/src/nonoka_cli/skills/__init__.py +0 -0
  94. {nonoka_cli-0.2.5 → nonoka_cli-0.2.6}/src/nonoka_cli/tools/__init__.py +0 -0
  95. {nonoka_cli-0.2.5 → nonoka_cli-0.2.6}/src/nonoka_cli/tools/builtins/__init__.py +0 -0
  96. {nonoka_cli-0.2.5 → nonoka_cli-0.2.6}/src/nonoka_cli/tools/builtins/file_tools.py +0 -0
  97. {nonoka_cli-0.2.5 → nonoka_cli-0.2.6}/src/nonoka_cli/tools/loader.py +0 -0
  98. {nonoka_cli-0.2.5 → nonoka_cli-0.2.6}/src/nonoka_cli/utils/__init__.py +0 -0
  99. {nonoka_cli-0.2.5 → nonoka_cli-0.2.6}/src/nonoka_cli/utils/errors.py +0 -0
  100. {nonoka_cli-0.2.5 → nonoka_cli-0.2.6}/src/nonoka_cli/utils/logging.py +0 -0
  101. {nonoka_cli-0.2.5 → nonoka_cli-0.2.6}/src/nonoka_cli/utils/trace_logger.py +0 -0
  102. {nonoka_cli-0.2.5 → nonoka_cli-0.2.6}/tests/conftest.py +0 -0
  103. {nonoka_cli-0.2.5 → nonoka_cli-0.2.6}/tests/integration/test_server_external_capabilities.py +0 -0
  104. {nonoka_cli-0.2.5 → nonoka_cli-0.2.6}/tests/unit/bridge/test_events.py +0 -0
  105. {nonoka_cli-0.2.5 → nonoka_cli-0.2.6}/tests/unit/bridge/test_handler.py +0 -0
  106. {nonoka_cli-0.2.5 → nonoka_cli-0.2.6}/tests/unit/bridge/test_protocol.py +0 -0
  107. {nonoka_cli-0.2.5 → nonoka_cli-0.2.6}/tests/unit/commands/test_config_cmd.py +0 -0
  108. {nonoka_cli-0.2.5 → nonoka_cli-0.2.6}/tests/unit/commands/test_doctor_cmd.py +0 -0
  109. {nonoka_cli-0.2.5 → nonoka_cli-0.2.6}/tests/unit/commands/test_run_cmd.py +0 -0
  110. {nonoka_cli-0.2.5 → nonoka_cli-0.2.6}/tests/unit/config/test_loader.py +0 -0
  111. {nonoka_cli-0.2.5 → nonoka_cli-0.2.6}/tests/unit/core/test_context_trimmer.py +0 -0
  112. {nonoka_cli-0.2.5 → nonoka_cli-0.2.6}/tests/unit/core/test_git_service.py +0 -0
  113. {nonoka_cli-0.2.5 → nonoka_cli-0.2.6}/tests/unit/core/test_plugin_manifest.py +0 -0
  114. {nonoka_cli-0.2.5 → nonoka_cli-0.2.6}/tests/unit/core/test_plugin_manifest_converter.py +0 -0
  115. {nonoka_cli-0.2.5 → nonoka_cli-0.2.6}/tests/unit/core/test_repo_map_service.py +0 -0
  116. {nonoka_cli-0.2.5 → nonoka_cli-0.2.6}/tests/unit/core/test_task_state.py +0 -0
  117. {nonoka_cli-0.2.5 → nonoka_cli-0.2.6}/tests/unit/core/test_tool_output_policy.py +0 -0
  118. {nonoka_cli-0.2.5 → nonoka_cli-0.2.6}/tests/unit/sessions/test_manager.py +0 -0
  119. {nonoka_cli-0.2.5 → nonoka_cli-0.2.6}/tests/unit/utils/test_trace_logger.py +0 -0
  120. {nonoka_cli-0.2.5 → nonoka_cli-0.2.6}/uv.lock +0 -0
@@ -61,4 +61,7 @@ verdaccio-storage/
61
61
 
62
62
  # Private planning documents
63
63
  DESIGN.md
64
- REFACTOR.md
64
+ REFACTOR.md
65
+ # Runtime nonoka project artifacts
66
+ .nonoka/repo_map.jsonl
67
+ .nonoka/tasks/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nonoka-cli
3
- Version: 0.2.5
3
+ Version: 0.2.6
4
4
  Summary: OpenCode backend for the Nonoka Agent framework
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "nonoka-cli"
7
- version = "0.2.5"
7
+ version = "0.2.6"
8
8
  description = "OpenCode backend for the Nonoka Agent framework"
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -138,9 +138,7 @@ async def _async_main(
138
138
  ) -> int:
139
139
  """Set up async stdin/stdout and run the bridge server."""
140
140
  loop = asyncio.get_event_loop()
141
- # Large prompts (repo map, file contents) can produce single NDJSON lines
142
- # well above the default 64 KiB StreamReader limit. Bump it to 10 MiB.
143
- stdin = asyncio.StreamReader(limit=10 * 1024 * 1024)
141
+ stdin = asyncio.StreamReader()
144
142
  await loop.connect_read_pipe(lambda: asyncio.StreamReaderProtocol(stdin), sys.stdin)
145
143
 
146
144
  w_transport, w_protocol = await loop.connect_write_pipe(
@@ -54,8 +54,9 @@ def _build_opencode_permission(config: CLIConfig) -> dict[str, str]:
54
54
  permission[tool] = action
55
55
 
56
56
  # nonoka.yaml permissions take precedence over auto_approve defaults.
57
- if config.permissions:
58
- permission.update(config.permissions)
57
+ user_permissions = getattr(config, "permissions", None)
58
+ if user_permissions:
59
+ permission.update(user_permissions)
59
60
  return permission
60
61
 
61
62
 
@@ -111,20 +111,16 @@ class RepoMapConfig(BaseModel):
111
111
 
112
112
 
113
113
  class AgentRoleConfig(BaseModel):
114
- """Configuration for a single agent role (planner / reviewer)."""
114
+ """Configuration for a single agent role (planner / executor)."""
115
115
  model: str = ""
116
116
  max_turns: int = 5
117
117
  system_prompt: str = ""
118
118
 
119
119
 
120
120
  class AgentsConfig(BaseModel):
121
- """Multi-agent role configuration.
122
-
123
- The ``executor`` role has been removed: the main agent is the executor, and
124
- its ``max_turns`` is configured at the top level of ``CLIConfig``.
125
- """
121
+ """Multi-agent role configuration."""
126
122
  planner: AgentRoleConfig = Field(default_factory=AgentRoleConfig)
127
- reviewer: AgentRoleConfig = Field(default_factory=AgentRoleConfig)
123
+ executor: AgentRoleConfig = Field(default_factory=AgentRoleConfig)
128
124
 
129
125
 
130
126
  class PluginConfig(BaseModel):
@@ -146,11 +142,9 @@ class CLIConfig(BaseModel):
146
142
  Matches the expected structure of ~/.config/nonoka/config.yaml.
147
143
  """
148
144
  model: str = "gpt-4o"
149
- max_turns: int = 20
150
145
  system_prompt: str = ""
151
146
  api_key: str = ""
152
147
  base_url: str = ""
153
- permissions: dict[str, str] = Field(default_factory=dict)
154
148
  mcp_servers: dict[str, MCPServerConfigModel] = Field(default_factory=dict)
155
149
  tool_paths: list[Path] = Field(default_factory=list)
156
150
  skills: list[str] = Field(default_factory=list)
@@ -26,6 +26,7 @@ from nonoka_cli.config.models import CLIConfig
26
26
  from nonoka_cli.core.namespaces import (
27
27
  PrefixedCapability,
28
28
  mcp_tool_name,
29
+ sanitize_tool_name,
29
30
  skill_tool_name,
30
31
  )
31
32
  from nonoka_cli.core.prompt_builder import SystemPromptBuilder
@@ -143,8 +144,11 @@ class AgentFactory:
143
144
  return self._skill_registry
144
145
 
145
146
  def _executor_max_turns(self) -> int:
146
- """Return the configured main-agent max turns."""
147
- return self._config.max_turns
147
+ """Return the configured executor max turns, falling back to 20."""
148
+ configured = getattr(
149
+ self._config.agents.executor, "max_turns", None
150
+ )
151
+ return configured if configured else 20
148
152
 
149
153
  def build(
150
154
  self,
@@ -153,7 +157,6 @@ class AgentFactory:
153
157
  git_summary: str | None = None,
154
158
  plugin_summary: str | None = None,
155
159
  execution_plan: str | None = None,
156
- extra_tools: list[Capability] | None = None,
157
160
  ) -> Agent:
158
161
  """Build (or rebuild) an Agent from the current configuration.
159
162
 
@@ -164,9 +167,6 @@ class AgentFactory:
164
167
  repo_map: Optional repo-map block to inject into the system prompt.
165
168
  git_summary: Optional git-checkpoint status summary.
166
169
  plugin_summary: Optional loaded plugin manifest summary.
167
- execution_plan: Optional structured plan produced by the planner agent.
168
- extra_tools: Optional additional tools (e.g. sub-agent AgentTools) to
169
- register with the Agent.
170
170
 
171
171
  Returns:
172
172
  A nonoka Agent instance.
@@ -233,12 +233,6 @@ class AgentFactory:
233
233
  builder = builder.tool(capability)
234
234
  logger.debug("agent_factory_mcp_tools", count=len(mcp_tools))
235
235
 
236
- # Extra tools (e.g. planner/reviewer sub-agents) injected by Orchestrator.
237
- if extra_tools:
238
- for tool in extra_tools:
239
- builder = builder.tool(tool)
240
- logger.debug("agent_factory_extra_tools", count=len(extra_tools))
241
-
242
236
  self._agent = builder.build()
243
237
  return self._agent
244
238
 
@@ -274,7 +268,6 @@ class AgentFactory:
274
268
  git_summary: str | None = None,
275
269
  plugin_summary: str | None = None,
276
270
  execution_plan: str | None = None,
277
- extra_tools: list[Capability] | None = None,
278
271
  ) -> Agent:
279
272
  """Build an Agent for host-managed (e.g. OpenCode) mode.
280
273
 
@@ -296,9 +289,6 @@ class AgentFactory:
296
289
  in nonoka.yaml.
297
290
  external_mcp_servers: Optional host-managed MCP server definitions.
298
291
  external_skills: Optional host-managed skill definitions.
299
- execution_plan: Optional structured plan produced by the planner agent.
300
- extra_tools: Optional additional tools (e.g. sub-agent AgentTools) to
301
- register with the Agent.
302
292
  """
303
293
  if not self._config.model:
304
294
  raise AgentBuildError("No model configured. Set 'model' in config.yaml.")
@@ -454,12 +444,6 @@ class AgentFactory:
454
444
  for server, cap in mcp_tools:
455
445
  builder = builder.tool(PrefixedCapability(cap, f"mcp__{server}__"))
456
446
 
457
- # Extra tools (e.g. planner/reviewer sub-agents) injected by Orchestrator.
458
- if extra_tools:
459
- for tool in extra_tools:
460
- builder = builder.tool(tool)
461
- logger.debug("agent_factory_extra_tools", count=len(extra_tools))
462
-
463
447
  return builder.build()
464
448
 
465
449
  def _skill_registry_for_build(
@@ -26,7 +26,6 @@ from nonoka_cli.core.context_trimmer import TurnBasedContextTrimmer
26
26
  from nonoka_cli.core.git_service import GitService, build_git_service
27
27
  from nonoka_cli.core.mcp_service import MCPService
28
28
  from nonoka_cli.core.planning_service import PlanningService, build_planning_service
29
- from nonoka_cli.core.review_service import ReviewService, build_review_service
30
29
  from nonoka_cli.core.plugin_manifest import (
31
30
  PluginManifestLoader,
32
31
  format_manifest_summary,
@@ -99,7 +98,6 @@ class Orchestrator:
99
98
  self._git_service: GitService | None = None
100
99
  self._repo_map_service: RepoMapService | None = None
101
100
  self._planning_service: PlanningService | None = None
102
- self._review_service: ReviewService | None = None
103
101
  self._plugin_manifests: list[Any] = []
104
102
  self._initialized = False
105
103
 
@@ -162,10 +160,6 @@ class Orchestrator:
162
160
  working_dir=Path.cwd(),
163
161
  config=self._config,
164
162
  )
165
- self._review_service = build_review_service(
166
- working_dir=Path.cwd(),
167
- config=self._config,
168
- )
169
163
 
170
164
  # Load project plugin manifests before building the Agent so that
171
165
  # allowed_tools / plugin summary can be taken into account.
@@ -191,7 +185,6 @@ class Orchestrator:
191
185
  repo_map=repo_map_block,
192
186
  git_summary=git_summary,
193
187
  plugin_summary=plugin_summary,
194
- extra_tools=self._subagent_tools(),
195
188
  )
196
189
  self._tool_service = ToolService(self._agent_factory, self._tool_loader)
197
190
  self._mcp_service = MCPService(self._mcp_manager, self._agent_factory)
@@ -354,19 +347,6 @@ class Orchestrator:
354
347
  merged = merge_manifests(self._plugin_manifests)
355
348
  return merged.allowed_tools
356
349
 
357
- def _subagent_tools(self) -> list[Any]:
358
- """Return enabled sub-agent AgentTools for injection into the executor.
359
-
360
- Only planner/reviewer tools whose model is configured are returned, so the
361
- default executor tool list stays small.
362
- """
363
- tools: list[Any] = []
364
- if self._planning_service is not None and self._planning_service.enabled:
365
- tools.append(self._planning_service.agent_tool)
366
- if self._review_service is not None and self._review_service.enabled:
367
- tools.append(self._review_service.agent_tool)
368
- return tools
369
-
370
350
  def _on_config_changed(self, config: CLIConfig) -> None:
371
351
  """ConfigManager hot-reload listener: update local reference only."""
372
352
  self._config = config
@@ -450,10 +430,7 @@ class Orchestrator:
450
430
  logger.info("execute_plan_result", plan_preview=plan[:200] if plan else None)
451
431
  if plan and not plan.startswith(("Error", "Planning is disabled")):
452
432
  logger.info("execution_plan_generated", plan_length=len(plan))
453
- agent = self._agent_factory.build(
454
- execution_plan=plan,
455
- extra_tools=self._subagent_tools(),
456
- )
433
+ agent = self._agent_factory.build(execution_plan=plan)
457
434
  except Exception as exc:
458
435
  logger.warning("execution_plan_generation_failed", error=str(exc))
459
436
 
@@ -560,7 +537,6 @@ class Orchestrator:
560
537
  git_summary=git_summary,
561
538
  plugin_summary=plugin_summary,
562
539
  execution_plan=execution_plan,
563
- extra_tools=self._subagent_tools(),
564
540
  )
565
541
 
566
542
  logger.info(
@@ -618,7 +594,6 @@ class Orchestrator:
618
594
  repo_map=repo_map_block,
619
595
  git_summary=git_summary,
620
596
  plugin_summary=plugin_summary,
621
- extra_tools=self._subagent_tools(),
622
597
  )
623
598
 
624
599
  logger.info(
@@ -0,0 +1,89 @@
1
+ """Planning service for nonoka-cli.
2
+
3
+ Wires planner/executor configuration (``agents.planner``) to the orchestrator.
4
+ The actual planner implementation has been removed from nonoka-agent in 1.3.4;
5
+ nonoka-cli will provide its own LLM-based planner here in a future release.
6
+ For now the service reports itself as disabled when invoked so the orchestrator
7
+ falls back to single-agent execution.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from pathlib import Path
13
+
14
+ from nonoka_cli.config.models import AgentsConfig, CLIConfig
15
+
16
+
17
+ class PlanningService:
18
+ """Adapts the nonoka-agent planner tool to CLI config."""
19
+
20
+ def __init__(
21
+ self,
22
+ working_dir: Path,
23
+ config: AgentsConfig | None = None,
24
+ default_model: str | None = None,
25
+ ):
26
+ self._working_dir = working_dir
27
+ self._config = config or AgentsConfig()
28
+ self._default_model = default_model
29
+
30
+ @property
31
+ def enabled(self) -> bool:
32
+ return bool(self.planner_model)
33
+
34
+ @property
35
+ def planner_model(self) -> str:
36
+ """Return the effective planner model identifier."""
37
+ configured = self._config.planner.model
38
+ if configured:
39
+ return configured
40
+ if self._default_model:
41
+ return self._default_model
42
+ return ""
43
+
44
+ @property
45
+ def executor_model(self) -> str:
46
+ """Return the effective executor model identifier."""
47
+ configured = self._config.executor.model
48
+ if configured:
49
+ return configured
50
+ if self._default_model:
51
+ return self._default_model
52
+ return ""
53
+
54
+ @property
55
+ def planner_max_turns(self) -> int:
56
+ return self._config.planner.max_turns
57
+
58
+ @property
59
+ def executor_max_turns(self) -> int:
60
+ return self._config.executor.max_turns
61
+
62
+ async def plan(self, task: str, max_steps: int = 10) -> str:
63
+ """Generate a structured plan for *task*.
64
+
65
+ Returns a human-readable plan or an error message. The planner tool has
66
+ been removed from nonoka-agent in 1.3.4; a CLI-native planner will replace
67
+ this stub in a future release.
68
+ """
69
+ if not self.enabled:
70
+ return "Planning is disabled: no planner model configured."
71
+ return (
72
+ "Planning is disabled: the built-in planner tool was removed from "
73
+ "nonoka-agent 1.3.4. Configure a planner model to opt into the future "
74
+ "CLI-native planner implementation."
75
+ )
76
+
77
+
78
+ def build_planning_service(
79
+ working_dir: Path,
80
+ config: CLIConfig | None = None,
81
+ ) -> PlanningService:
82
+ """Factory for creating a PlanningService from CLI configuration."""
83
+ agents_config = config.agents if config is not None else AgentsConfig()
84
+ default_model = config.model if config is not None else None
85
+ return PlanningService(
86
+ working_dir=working_dir,
87
+ config=agents_config,
88
+ default_model=default_model,
89
+ )
@@ -181,20 +181,11 @@ class SystemPromptBuilder:
181
181
  def _build_planner_block(self) -> str:
182
182
  """Return guidance for the planner/executor workflow.
183
183
 
184
- The ``plan_task`` sub-agent tool is injected only when ``agents.planner.model``
185
- is configured. The workflow reminder is included so the model knows to use
186
- planning for complex tasks and to synchronize progress with OpenCode's TODO
187
- UI via ``todowrite``.
184
+ The built-in ``plan_task`` tool was removed from nonoka-agent in 1.3.4; the
185
+ planner block is currently suppressed until nonoka-cli provides its own
186
+ LLM-based planner implementation.
188
187
  """
189
- if "plan_task" in self._base.lower():
190
- return ""
191
- return (
192
- "## Planner / Executor Workflow\n"
193
- "For complex multi-file or multi-step tasks, first call the ``plan_task`` "
194
- "tool to produce a structured plan. Then execute the steps using the "
195
- "file, search, and run-command tools. After each step, call ``todowrite`` "
196
- "to update the TODO list so the user can follow progress in the OpenCode TUI."
197
- )
188
+ return ""
198
189
 
199
190
  def _build_execution_plan_block(self) -> str:
200
191
  """Return the concrete execution plan produced by the planner agent."""
@@ -1,9 +1,9 @@
1
1
  """Agent-level tools re-exported from nonoka-agent.
2
2
 
3
3
  These tools are implemented in nonoka-agent (the core agent framework) and are
4
- made available to every nonoka-cli Agent. The ``plan_task`` tool has been moved
5
- to ``nonoka_cli.core.planning_service`` and is injected as a sub-agent AgentTool
6
- by the Orchestrator when ``agents.planner.model`` is configured.
4
+ made available to every nonoka-cli Agent. Keeping the implementation in
5
+ nonoka-agent lets other consumers reuse them, while this module declares the
6
+ subset the CLI exposes by default.
7
7
  """
8
8
 
9
9
  from __future__ import annotations
@@ -151,28 +151,3 @@ def test_opencode_init_disables_native_skill_tool(tmp_path: Path):
151
151
 
152
152
  data = json.loads((tmp_path / "opencode.json").read_text())
153
153
  assert data.get("tools", {}).get("skill") is False
154
-
155
-
156
- def test_opencode_init_permissions_override(tmp_path: Path):
157
- config_path = tmp_path / "nonoka.yaml"
158
- ConfigLoader.save(
159
- CLIConfig(
160
- model="deepseek-chat",
161
- permissions={"read": "allow", "bash": "ask"},
162
- ),
163
- config_path,
164
- )
165
-
166
- args = argparse.Namespace(
167
- config=str(config_path),
168
- cwd=str(tmp_path),
169
- global_=False,
170
- )
171
- assert cmd_init(args) == 0
172
-
173
- data = json.loads((tmp_path / "opencode.json").read_text())
174
- assert data["permission"]["read"] == "allow"
175
- assert data["permission"]["bash"] == "ask"
176
- assert data["permission"]["write"] == "ask"
177
- assert data["agent"]["build"]["permission"]["read"] == "allow"
178
- assert data["agent"]["build"]["permission"]["bash"] == "ask"
@@ -1,7 +1,6 @@
1
1
  from pathlib import Path
2
2
 
3
3
  from nonoka_cli.config.models import (
4
- AgentsConfig,
5
4
  CLIBehaviorConfig,
6
5
  CLIConfig,
7
6
  HITLConfigModel,
@@ -21,9 +20,7 @@ def test_cli_behavior_defaults():
21
20
  def test_cli_config_defaults():
22
21
  cfg = CLIConfig()
23
22
  assert cfg.model == "gpt-4o"
24
- assert cfg.max_turns == 20
25
23
  assert cfg.system_prompt == ""
26
- assert cfg.permissions == {}
27
24
  assert cfg.mcp_servers == {}
28
25
  assert cfg.tool_paths == []
29
26
  assert cfg.skills == []
@@ -31,30 +28,6 @@ def test_cli_config_defaults():
31
28
  assert isinstance(cfg.hitl, HITLConfigModel)
32
29
 
33
30
 
34
- def test_agents_config_has_no_executor():
35
- cfg = AgentsConfig()
36
- assert hasattr(cfg, "planner")
37
- assert hasattr(cfg, "reviewer")
38
- assert not hasattr(cfg, "executor")
39
-
40
-
41
- def test_cli_config_ignores_legacy_executor():
42
- # Existing nonoka.yaml files may still contain agents.executor; Pydantic
43
- # should ignore it rather than crash.
44
- data = {
45
- "model": "deepseek-chat",
46
- "agents": {
47
- "planner": {"model": "gpt-4o-mini"},
48
- "executor": {"model": "gpt-4o", "max_turns": 10},
49
- "reviewer": {"model": "gpt-4o-mini"},
50
- },
51
- }
52
- cfg = CLIConfig.model_validate(data)
53
- assert cfg.max_turns == 20
54
- assert cfg.agents.planner.model == "gpt-4o-mini"
55
- assert not hasattr(cfg.agents, "executor")
56
-
57
-
58
31
  def test_cli_config_tool_paths_expanded():
59
32
  cfg = CLIConfig(tool_paths=["~/tools", "./local_tools"])
60
33
  assert cfg.tool_paths[0] == Path.home() / "tools"
@@ -415,49 +415,3 @@ async def test_build_with_external_tools_no_warning_when_native_skill_disabled(t
415
415
  agent = factory.build_with_external_tools(tools, cwd=str(tmp_path))
416
416
  prompt = agent.system_prompt
417
417
  assert "OpenCode's native skill tool is enabled" not in prompt
418
-
419
-
420
- class _FakeExtraTool:
421
- """Minimal capability stand-in for extra_tools injection tests."""
422
-
423
- name = "extra_tool"
424
- description = "An extra tool"
425
-
426
- @property
427
- def parameters(self) -> dict:
428
- return {"type": "object", "properties": {}}
429
-
430
- async def invoke(self, ctx, arguments):
431
- return "ok"
432
-
433
- def to_json_schema(self):
434
- return {
435
- "type": "function",
436
- "function": {"name": self.name, "description": self.description, "parameters": {}},
437
- }
438
-
439
-
440
- @pytest.mark.asyncio
441
- async def test_build_injects_extra_tools():
442
- config = CLIConfig(model="gpt-4o")
443
- factory = AgentFactory(config)
444
- agent = factory.build(extra_tools=[_FakeExtraTool()])
445
- tool_names = {t.name for t in agent.tools}
446
- assert "extra_tool" in tool_names
447
-
448
-
449
- @pytest.mark.asyncio
450
- async def test_build_with_external_tools_injects_extra_tools():
451
- config = CLIConfig(model="gpt-4o")
452
- factory = AgentFactory(config)
453
- tools = [
454
- AgentFactory.create_external_tool_capability(
455
- name="bash",
456
- description="Run shell commands",
457
- parameters={"type": "object", "properties": {}},
458
- )
459
- ]
460
- agent = factory.build_with_external_tools(tools, extra_tools=[_FakeExtraTool()])
461
- tool_names = {t.name for t in agent.tools}
462
- assert "bash" in tool_names
463
- assert "extra_tool" in tool_names
@@ -0,0 +1,98 @@
1
+ """Tests for Orchestrator two-stage planning integration."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from types import SimpleNamespace
7
+ from unittest.mock import AsyncMock, MagicMock, patch
8
+
9
+ import pytest
10
+
11
+ from nonoka_cli.config.models import CLIConfig
12
+ from nonoka_cli.core.orchestrator import Orchestrator
13
+
14
+
15
+ @pytest.fixture
16
+ def base_orchestrator(tmp_path: Path) -> Orchestrator:
17
+ """Return a minimally initialized orchestrator with mocked services."""
18
+ config = CLIConfig(
19
+ model="gpt-4o",
20
+ agents={
21
+ "planner": {"model": "gpt-4o-mini", "max_turns": 3, "system_prompt": ""},
22
+ "executor": {"model": "gpt-4o", "max_turns": 10, "system_prompt": ""},
23
+ },
24
+ )
25
+ orch = Orchestrator(config=config)
26
+ orch._initialized = True
27
+ orch._session_service = MagicMock()
28
+ orch._session_service.current_id = "test-session"
29
+ orch._session_service.manager = SimpleNamespace(db_path=tmp_path / "nonoka.db")
30
+ orch._runner_service = MagicMock()
31
+ orch._agent_factory = MagicMock()
32
+ orch._agent_factory.get_agent.return_value = MagicMock()
33
+ orch._config = config
34
+ return orch
35
+
36
+
37
+ @pytest.mark.asyncio
38
+ async def test_execute_with_external_tools_injects_execution_plan(
39
+ base_orchestrator: Orchestrator,
40
+ ) -> None:
41
+ """When planning is enabled, the generated plan is passed to the agent factory."""
42
+ orch = base_orchestrator
43
+
44
+ planning_service = MagicMock()
45
+ planning_service.enabled = True
46
+ planning_service.planner_model = "gpt-4o-mini"
47
+ planning_service.plan = AsyncMock(return_value="1. Do X\n2. Do Y")
48
+ orch._planning_service = planning_service
49
+
50
+ async def _fake_stream(*args, **kwargs):
51
+ yield MagicMock(type="final", data={"success": True})
52
+
53
+ orch._runner_service.run = MagicMock(side_effect=_fake_stream)
54
+
55
+ events = []
56
+ async for event in orch.execute_with_external_tools(
57
+ prompt="Create a test file",
58
+ tools=[],
59
+ working_dir=Path.cwd(),
60
+ host_system_prompt="Host prompt",
61
+ ):
62
+ events.append(event)
63
+
64
+ planning_service.plan.assert_awaited_once_with("Create a test file")
65
+ call_kwargs = orch._agent_factory.build_with_external_tools.call_args.kwargs
66
+ assert call_kwargs.get("execution_plan") == "1. Do X\n2. Do Y"
67
+
68
+
69
+ @pytest.mark.asyncio
70
+ async def test_execute_with_external_tools_skips_planning_when_disabled(
71
+ base_orchestrator: Orchestrator,
72
+ ) -> None:
73
+ """When no planner model is configured, no plan is generated."""
74
+ orch = base_orchestrator
75
+
76
+ planning_service = MagicMock()
77
+ planning_service.enabled = False
78
+ planning_service.planner_model = ""
79
+ planning_service.plan = AsyncMock(return_value="should not be used")
80
+ orch._planning_service = planning_service
81
+
82
+ async def _fake_stream(*args, **kwargs):
83
+ yield MagicMock(type="final", data={"success": True})
84
+
85
+ orch._runner_service.run = MagicMock(side_effect=_fake_stream)
86
+
87
+ events = []
88
+ async for event in orch.execute_with_external_tools(
89
+ prompt="Create a test file",
90
+ tools=[],
91
+ working_dir=Path.cwd(),
92
+ host_system_prompt="Host prompt",
93
+ ):
94
+ events.append(event)
95
+
96
+ planning_service.plan.assert_not_awaited()
97
+ call_kwargs = orch._agent_factory.build_with_external_tools.call_args.kwargs
98
+ assert call_kwargs.get("execution_plan") is None
@@ -0,0 +1,40 @@
1
+ """Tests for PlanningService."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ import pytest
8
+
9
+ from nonoka_cli.config.models import AgentsConfig, AgentRoleConfig, CLIConfig
10
+ from nonoka_cli.core.planning_service import PlanningService, build_planning_service
11
+
12
+
13
+ def test_planner_model_prefers_configured() -> None:
14
+ config = AgentsConfig(
15
+ planner=AgentRoleConfig(model="gpt-4o-mini"),
16
+ executor=AgentRoleConfig(model="gpt-4o"),
17
+ )
18
+ service = PlanningService(working_dir=Path("."), config=config, default_model="fallback")
19
+ assert service.planner_model == "gpt-4o-mini"
20
+ assert service.executor_model == "gpt-4o"
21
+
22
+
23
+ def test_planner_model_falls_back_to_default() -> None:
24
+ config = AgentsConfig()
25
+ service = PlanningService(working_dir=Path("."), config=config, default_model="fallback")
26
+ assert service.planner_model == "fallback"
27
+
28
+
29
+ def test_disabled_when_no_model() -> None:
30
+ service = PlanningService(working_dir=Path("."), config=AgentsConfig())
31
+ assert not service.enabled
32
+
33
+
34
+ def test_build_from_config() -> None:
35
+ config = CLIConfig(model="base-model", agents=AgentsConfig(
36
+ planner=AgentRoleConfig(model="planner-model"),
37
+ ))
38
+ service = build_planning_service(working_dir=Path("."), config=config)
39
+ assert service.planner_model == "planner-model"
40
+ assert service.executor_model == "base-model"