bone-agent 1.3.3 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +17 -0
  2. package/config.yaml.example +5 -2
  3. package/package.json +1 -1
  4. package/prompts/main/communication_style.md +1 -1
  5. package/prompts/main/dream.md +23 -9
  6. package/prompts/main/skills.md +3 -0
  7. package/prompts/micro/communication_style.md +1 -1
  8. package/prompts/micro/skills.md +1 -0
  9. package/src/core/agentic.py +138 -38
  10. package/src/core/chat_manager.py +19 -6
  11. package/src/core/config_manager.py +8 -1
  12. package/src/core/cron.py +0 -4
  13. package/src/core/metadata.py +75 -0
  14. package/src/core/skills.py +463 -0
  15. package/src/core/sub_agent.py +93 -43
  16. package/src/core/tool_feedback.py +87 -76
  17. package/src/llm/client.py +7 -2
  18. package/src/llm/codex_provider.py +350 -0
  19. package/src/llm/config.py +46 -2
  20. package/src/llm/prompts.py +12 -7
  21. package/src/llm/providers.py +3 -1
  22. package/src/llm/token_tracker.py +15 -0
  23. package/src/tools/__init__.py +24 -85
  24. package/src/tools/create_file.py +1 -1
  25. package/src/tools/directory.py +1 -1
  26. package/src/tools/edit.py +5 -1
  27. package/src/tools/file_reader.py +1 -1
  28. package/src/tools/helpers/__init__.py +1 -7
  29. package/src/tools/helpers/base.py +65 -16
  30. package/src/tools/helpers/loader.py +2 -88
  31. package/src/tools/helpers/path_resolver.py +54 -3
  32. package/src/tools/helpers/plugin_manifest.py +99 -70
  33. package/src/tools/review_sub_agent.py +2 -1
  34. package/src/tools/rg_search.py +24 -7
  35. package/src/tools/search_plugins.py +140 -72
  36. package/src/tools/shell.py +3 -3
  37. package/src/ui/commands.py +355 -33
  38. package/src/ui/displays.py +26 -1
  39. package/src/ui/main.py +0 -4
  40. package/src/ui/tool_confirmation.py +16 -5
  41. package/src/utils/editor.py +88 -39
  42. package/src/utils/settings.py +6 -2
  43. package/src/utils/validation.py +10 -0
package/README.md CHANGED
@@ -130,8 +130,25 @@ bone
130
130
  - `/account` - View your bone-agent account and plan details
131
131
  - `/plan` - View available plans and pricing
132
132
  - `/upgrade` - Upgrade your subscription
133
+ - `/skills list [query]` - List saved skills, optionally filtered by name or content
134
+ - `/skills show <name>` - Display a saved skill
135
+ - `/skills add <name>` - Create a reusable prompt skill in your editor
136
+ - `/skills edit <name>` - Open a saved skill in your editor
137
+ - `/skills modify <name> [prompt]` - Update an existing saved skill inline or in your editor
138
+ - `/skills load <name>` - Load a saved skill into the current chat
139
+ - `/skills use <name>` - Alias for `/skills load`
140
+ - `/skills remove <name>` - Delete a saved skill
141
+ - `/skills dir` - Print the skills directory path
133
142
  - `/help` - Display all available commands
134
143
 
144
+ Example:
145
+
146
+ ```text
147
+ /skills add frontend_design
148
+ /skills modify frontend_design Use restrained, production-quality UI patterns.
149
+ /skills use frontend_design
150
+ ```
151
+
135
152
  /help Menu:
136
153
  <img width="1843" height="1349" alt="image" src="https://github.com/user-attachments/assets/631ab805-f012-4bb6-a031-c82a339e94c5" />
137
154
 
@@ -104,8 +104,11 @@ CONTEXT_SETTINGS:
104
104
  # -- Sub-Agent Settings -------------------------------------------------------
105
105
 
106
106
  SUB_AGENT_SETTINGS:
107
- soft_limit_tokens: 300000
108
- hard_limit_tokens: 500000
107
+ soft_limit_tokens: 100000
108
+ hard_limit_tokens: 150000
109
+ billed_warning_tokens: 200000
110
+ billed_hard_limit_tokens: 500000
111
+ compact_trigger_tokens: 50000
109
112
  enable_compaction: false
110
113
  dump_context_on_hard_limit: true
111
114
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bone-agent",
3
- "version": "1.3.3",
3
+ "version": "1.4.0",
4
4
  "description": "A terminal-based AI coding assistant powered by OpenAI-style function calling",
5
5
  "main": "src/ui/main.py",
6
6
  "bin": {
@@ -3,7 +3,7 @@
3
3
  **Important:** Default to concise explanations
4
4
 
5
5
  - Show only changed code snippets when making edits via tools, never in explanations
6
- - Use bullet points instead of prose when possible
6
+ - Use bullet points where necessary, not as default
7
7
  - Target: 3-5 sentences max for explanations, 10-15 lines max for plans
8
8
  - Explain the "why" and "what", skip the "how" unless requested
9
9
 
@@ -10,13 +10,26 @@ You are the dream agent — a background process that consolidates user messages
10
10
  b. If the project directory is found, read its project memory at `{project_dir}/.bone/agents.md`
11
11
  c. If the project directory cannot be resolved, treat those messages as user-level only
12
12
  3. Read the current user memory at `~/.bone/user_memory.md`
13
- 4. Analyze the messages for:
14
- - Preferences (tools, languages, workflows, coding style)
15
- - Corrections or feedback the user gave
16
- - Patterns in how the user works
17
- - Decisions made about architecture or approach
18
- - Explicit requests to remember something
19
- 5. Consolidate findings into the memory files — merge with existing content, don't duplicate
13
+
14
+ ## What to remember
15
+
16
+ Memory exists to change how the agent behaves in future conversations. Before writing anything, ask: "Would knowing this actually change my behavior next time we talk?"
17
+
18
+ ### High-value write these
19
+ - Explicit "remember this" or "don't forget" requests
20
+ - Strong, repeated preferences the user has expressed multiple times or with emphasis
21
+ - Corrections the user gave after the agent did something wrong ("I don't like X, do Y instead")
22
+ - Hard constraints ("never do X", "always do Y")
23
+
24
+ ### Low-value — do NOT write these
25
+ - One-off casual remarks that weren't emphasized or repeated
26
+ - Descriptions of the user's workflow that are just normal tool usage
27
+ - Feature implementation history ("added X to the config command")
28
+ - Things the agent can infer from context or that apply to most users
29
+ - Multiple entries saying the same thing in different words
30
+
31
+ ### The bar
32
+ A single mention is usually not enough. Look for emphasis, repetition, or explicit instruction. When in doubt, don't write. Empty memory is better than noisy memory.
20
33
 
21
34
  ## Routing
22
35
 
@@ -27,10 +40,11 @@ You are the dream agent — a background process that consolidates user messages
27
40
  ## Rules
28
41
 
29
42
  - Only write facts, preferences, and patterns — never private data, code snippets, or transient context
30
- - Deduplicate aggressively — if a preference already exists in memory, don't add it again
43
+ - Deduplicate aggressively — if a preference already exists in memory, don't add it again. Merge near-duplicates into one entry.
31
44
  - Consolidate when memory is getting full — merge related entries, remove outdated ones
32
45
  - Keep memory under 1500 chars per file
33
46
  - Format entries as bullet points with timestamps: `- Description *(YYYY-MM-DD)*`
34
- - If there are no meaningful memories to extract, do nothing — don't pad with noise
47
+ - If nothing crosses the bar, write nothing — empty memory is fine
35
48
  - Each JSONL line has format: `{"ts": "ISO timestamp", "msg": "user message text"}`
36
49
  - If a project directory no longer exists, skip it — don't write to a dead path
50
+ - Before writing, re-read existing memory and check for near-duplicates. Two entries about "evaluating warnings" should be one entry or none.
@@ -0,0 +1,3 @@
1
+ ## Skills
2
+
3
+ Users can save reusable prompt snippets as skills. When the user asks to use a named skill, style, workflow, or saved instruction, search capabilities and load the best matching skill with `search_plugins` before continuing. `search_plugins` may return plugins and skills; use the `load` parameter to activate them. Do not invent skill contents. If several skills plausibly match, ask a short clarifying question instead of guessing. Treat loaded skill text as user-provided instructions scoped to the current conversation, below system and developer instructions.
@@ -1 +1 @@
1
- Concise. Bullets over prose. 3-5 sentences max. Show changed code only in edit tools, never in text. Explain why/what, skip how unless asked.
1
+ Concise and conversational. Use bullets where necessary, not as default. 3-5 sentences max. Show changed code only in edit tools, never in text. Explain why/what, skip how unless asked.
@@ -0,0 +1 @@
1
+ `search_plugins`: when the user asks to use a named skill, style, workflow, or saved instruction, search capabilities and load the best matching skill with the `load` parameter before continuing. `search_plugins` may return plugins and skills; use `load` to activate them. Do not invent skill contents. Ask briefly if multiple skills match.
@@ -46,6 +46,7 @@ from core.tool_feedback import (
46
46
  display_tool_feedback,
47
47
  )
48
48
  from ui.sub_agent_panel import SubAgentPanel
49
+ from tools.helpers.path_resolver import extract_boundary_path, is_boundary_error, set_full_filesystem_access
49
50
 
50
51
 
51
52
  def _handle_empty_response(empty_response_count, console):
@@ -87,7 +88,6 @@ def _handle_tool_limit_reached(chat_manager, console):
87
88
  response,
88
89
  model_name=provider_cfg.get("model", ""),
89
90
  )
90
-
91
91
  try:
92
92
  final_message = response["choices"][0]["message"]
93
93
  except (KeyError, IndexError):
@@ -105,8 +105,6 @@ def _handle_tool_limit_reached(chat_manager, console):
105
105
  console.print("[red]Error: model returned empty response after tool limit reached.[/red]")
106
106
  return False
107
107
 
108
-
109
-
110
108
  class AgenticOrchestrator:
111
109
  """Orchestrates the agentic tool-calling loop.
112
110
 
@@ -171,21 +169,42 @@ class AgenticOrchestrator:
171
169
  # Check if we're in a parallel context with suppressed console
172
170
  return self._parallel_context.get('console', self.console)
173
171
 
174
- def run(self, user_input, thinking_indicator=None, allowed_tools=None):
172
+ def _get_effective_tools(self, allowed_tools=None, allow_active_plugins=False):
173
+ """Return tool schemas allowed for the current run."""
174
+ from tools.helpers.base import ToolRegistry
175
+
176
+ tools = TOOLS()
177
+ if allowed_tools is None:
178
+ return tools
179
+
180
+ effective_names = set(allowed_tools)
181
+ if allow_active_plugins:
182
+ effective_names.update(ToolRegistry.active_plugin_names())
183
+
184
+ if not effective_names:
185
+ return []
186
+
187
+ return [tool for tool in tools if tool["function"]["name"] in effective_names]
188
+
189
+ def run(self, user_input, thinking_indicator=None, allowed_tools=None, allow_active_plugins=False):
175
190
  """Main orchestration loop.
176
191
 
177
192
  Args:
178
193
  user_input: User's input message
179
194
  thinking_indicator: Optional ThinkingIndicator instance
180
195
  allowed_tools: Optional list of allowed tool names (for research)
196
+ allow_active_plugins: Whether to include active plugin tools in restricted runs
181
197
  """
198
+ self._current_allowed_tools = allowed_tools
199
+ self._current_allow_active_plugins = allow_active_plugins
200
+
182
201
  # Append user message
183
202
  self.chat_manager.messages.append({"role": "user", "content": user_input})
184
203
 
185
204
  # Log user message
186
205
  self.chat_manager.log_message({"role": "user", "content": user_input})
187
206
 
188
- from tools.base import ToolRegistry
207
+ from tools.helpers.base import ToolRegistry
189
208
 
190
209
  while True:
191
210
  # Decrement plugin TTLs after previous iteration's tool execution.
@@ -195,7 +214,10 @@ class AgenticOrchestrator:
195
214
  self.console.print(f"[dim]Plugins evicted (TTL expired): {evicted}[/dim]")
196
215
 
197
216
  # Get response from LLM
198
- response = self._get_llm_response(allowed_tools=allowed_tools)
217
+ response = self._get_llm_response(
218
+ allowed_tools=allowed_tools,
219
+ allow_active_plugins=allow_active_plugins,
220
+ )
199
221
  if response is None:
200
222
  return
201
223
 
@@ -209,11 +231,16 @@ class AgenticOrchestrator:
209
231
  if self._handle_final_response(response, thinking_indicator):
210
232
  return
211
233
  else:
212
- should_exit = self._handle_tool_calls(response, thinking_indicator, allowed_tools)
234
+ should_exit = self._handle_tool_calls(
235
+ response,
236
+ thinking_indicator,
237
+ allowed_tools,
238
+ allow_active_plugins=allow_active_plugins,
239
+ )
213
240
  if should_exit:
214
241
  return
215
242
 
216
- def _get_llm_response(self, allowed_tools=None):
243
+ def _get_llm_response(self, allowed_tools=None, allow_active_plugins=False):
217
244
  """Get next LLM response with tool definitions.
218
245
 
219
246
  Includes automatic retry with live countdown for timeout/connection errors.
@@ -221,6 +248,7 @@ class AgenticOrchestrator:
221
248
 
222
249
  Args:
223
250
  allowed_tools: Optional list of allowed tool names (overrides mode-based filtering)
251
+ allow_active_plugins: Whether to include active plugin tools in restricted runs
224
252
 
225
253
  Returns:
226
254
  Response dict from LLM, or None if error occurred
@@ -229,19 +257,17 @@ class AgenticOrchestrator:
229
257
  self.chat_manager.ensure_context_fits(console=self.console)
230
258
 
231
259
  # Use allowed_tools if provided, otherwise use mode-based filtering
232
- if allowed_tools is not None:
233
- # Validate that allowed_tools is not empty
234
- if not allowed_tools:
235
- self.console.print("[red]Error: allowed_tools is empty[/red]")
236
- return None
237
- # TOOLS is a function, call it to get the list
238
- tools = [tool for tool in TOOLS() if tool["function"]["name"] in allowed_tools]
239
- # Log filtered tools for debugging
240
- if self.debug_mode:
241
- tool_names = [t["function"]["name"] for t in tools]
242
- self.console.print(f"[dim]Available tools: {tool_names}[/dim]")
243
- else:
244
- tools = TOOLS()
260
+ if allowed_tools is not None and not allowed_tools and not allow_active_plugins:
261
+ self.console.print("[red]Error: allowed_tools is empty[/red]")
262
+ return None
263
+
264
+ tools = self._get_effective_tools(
265
+ allowed_tools=allowed_tools,
266
+ allow_active_plugins=allow_active_plugins,
267
+ )
268
+ if allowed_tools is not None and self.debug_mode:
269
+ tool_names = [t["function"]["name"] for t in tools]
270
+ self.console.print(f"[dim]Available tools: {tool_names}[/dim]")
245
271
 
246
272
  # Retry loop for timeout/connection errors
247
273
  last_error = None
@@ -262,7 +288,21 @@ class AgenticOrchestrator:
262
288
  continue
263
289
  else:
264
290
  # Non-retryable error or final attempt exhausted
291
+ detail_lines = []
292
+ for key, value in getattr(e, "details", {}).items():
293
+ value_str = str(value)
294
+ if "\n" in value_str or key == "original_error":
295
+ detail_lines.append(f"{key}: {value_str}")
296
+ detailed_error = str(e)
297
+ if detail_lines:
298
+ detailed_error += "\n\n" + "\n\n".join(detail_lines)
299
+
300
+ if self.is_sub_agent:
301
+ raise LLMError(detailed_error, details=getattr(e, "details", {}))
302
+
265
303
  self.console.print(f"[red]LLM Error: {e}[/red]")
304
+ if detail_lines:
305
+ self.console.print(f"[dim]{detail_lines[0]}[/dim]", markup=False)
266
306
  return None
267
307
 
268
308
  # Successful response — parse and return
@@ -324,8 +364,11 @@ class AgenticOrchestrator:
324
364
  # NEW: Compact tool results after final answer (per-message compaction)
325
365
  self.chat_manager.compact_tool_results(skip_token_update=True)
326
366
 
327
- # Update context tokens with current mode's tools
328
- tools_for_mode = TOOLS()
367
+ # Update context tokens with current run's effective tools
368
+ tools_for_mode = self._get_effective_tools(
369
+ allowed_tools=getattr(self, "_current_allowed_tools", None),
370
+ allow_active_plugins=getattr(self, "_current_allow_active_plugins", False),
371
+ )
329
372
  self.chat_manager._update_context_tokens(tools_for_mode)
330
373
 
331
374
  self.console.print()
@@ -337,13 +380,14 @@ class AgenticOrchestrator:
337
380
  )
338
381
  return not should_continue
339
382
 
340
- def _handle_tool_calls(self, response, thinking_indicator, allowed_tools=None):
383
+ def _handle_tool_calls(self, response, thinking_indicator, allowed_tools=None, allow_active_plugins=False):
341
384
  """Process tool calls and display accompanying content.
342
385
 
343
386
  Args:
344
387
  response: Full message dict from LLM (includes content and tool_calls)
345
388
  thinking_indicator: Optional ThinkingIndicator instance
346
389
  allowed_tools: Optional list of allowed tool names
390
+ allow_active_plugins: Whether to allow active plugin tools in restricted runs
347
391
 
348
392
  Returns:
349
393
  True if should exit the orchestration loop
@@ -357,6 +401,8 @@ class AgenticOrchestrator:
357
401
  # This must happen BEFORE filtering so the LLM sees its original intent
358
402
  content = (response.get("content") or "").strip()
359
403
  assistant_msg = {"role": "assistant", "tool_calls": tool_calls}
404
+ if response.get("_responses_output"):
405
+ assistant_msg["_responses_output"] = response["_responses_output"]
360
406
  if content:
361
407
  assistant_msg["content"] = content
362
408
  self.chat_manager.messages.append(assistant_msg)
@@ -367,7 +413,7 @@ class AgenticOrchestrator:
367
413
  # This silently removes unknown tools or tools not in the allowed whitelist
368
414
  # to prevent error messages from reaching the user while allowing the agent
369
415
  # to continue with alternative tools.
370
- from tools.base import ToolRegistry
416
+ from tools.helpers.base import ToolRegistry
371
417
 
372
418
  filtered_calls = []
373
419
  filtered_tool_ids = [] # Track filtered tool IDs to provide feedback
@@ -384,10 +430,14 @@ class AgenticOrchestrator:
384
430
  filtered_tool_ids.append(tool_call.get("id"))
385
431
  continue
386
432
 
387
- # Check if tool is in allowed_tools whitelist (if provided)
388
- # Plugin-tier tools bypass the whitelist — they are already vetted
389
- # by the manifest and activated on-demand via search_plugins.
390
- if allowed_tools and function_name not in allowed_tools and not ToolRegistry.is_plugin_active(function_name):
433
+ # Check if tool is in the effective allowlist for this run.
434
+ effective_allowed_tools = None
435
+ if allowed_tools is not None:
436
+ effective_allowed_tools = set(allowed_tools)
437
+ if allow_active_plugins:
438
+ effective_allowed_tools.update(ToolRegistry.active_plugin_names())
439
+
440
+ if effective_allowed_tools is not None and function_name not in effective_allowed_tools:
391
441
  # Silent fail - skip this tool
392
442
  if self.debug_mode:
393
443
  self.console.print(f"[dim]Silently filtered non-allowed tool: {function_name}[/dim]")
@@ -542,8 +592,11 @@ class AgenticOrchestrator:
542
592
  # Compact completed tool blocks once after all tools complete
543
593
  self.chat_manager.compact_tool_results(skip_token_update=True)
544
594
 
545
- # Update context tokens with current mode's tools
546
- tools_for_mode = TOOLS()
595
+ # Update context tokens with current run's effective tools
596
+ tools_for_mode = self._get_effective_tools(
597
+ allowed_tools=getattr(self, "_current_allowed_tools", None),
598
+ allow_active_plugins=getattr(self, "_current_allow_active_plugins", False),
599
+ )
547
600
  self.chat_manager._update_context_tokens(tools_for_mode)
548
601
 
549
602
  # Pre-send guard: ensure context fits before next LLM call
@@ -563,7 +616,7 @@ class AgenticOrchestrator:
563
616
  """
564
617
  if not tool_calls:
565
618
  return False
566
- from tools.parallel_executor import ParallelToolExecutor, ToolCall
619
+ from tools.helpers.parallel_executor import ParallelToolExecutor, ToolCall
567
620
 
568
621
  # Suppress console output in handlers during parallel execution
569
622
  # We'll display results ourselves in order below
@@ -783,8 +836,11 @@ class AgenticOrchestrator:
783
836
  # after all parallel results are appended (safe — only compacts completed blocks)
784
837
  self.chat_manager.compact_tool_results(skip_token_update=True)
785
838
 
786
- # Update context tokens with current mode's tools
787
- tools_for_mode = TOOLS()
839
+ # Update context tokens with current run's effective tools
840
+ tools_for_mode = self._get_effective_tools(
841
+ allowed_tools=getattr(self, "_current_allowed_tools", None),
842
+ allow_active_plugins=getattr(self, "_current_allow_active_plugins", False),
843
+ )
788
844
  self.chat_manager._update_context_tokens(tools_for_mode)
789
845
 
790
846
  # Pre-send guard: ensure context fits before next LLM call
@@ -795,6 +851,38 @@ class AgenticOrchestrator:
795
851
  # Restore console output
796
852
  self._parallel_context['console'] = self.console
797
853
 
854
+ def _boundary_prompt(self, path_str):
855
+ """Prompt the user to grant filesystem access for a path outside boundaries.
856
+
857
+ Called after a tool returns a boundary error. If the user grants access,
858
+ the caller retries the tool with the boundary lifted.
859
+
860
+ Args:
861
+ path_str: The path that triggered the boundary violation.
862
+
863
+ Returns:
864
+ True if user granted access, False if denied.
865
+ """
866
+ if self.is_sub_agent:
867
+ return False
868
+
869
+ console = self._get_console()
870
+ if console is None:
871
+ return False
872
+
873
+ from ui.tool_confirmation import ToolConfirmationPanel
874
+ panel = ToolConfirmationPanel(
875
+ 'Grant filesystem access',
876
+ reason=f'Agent requested access outside project boundary: {path_str}',
877
+ is_edit_tool=False
878
+ )
879
+ action, _ = panel.run()
880
+
881
+ if action == "accept":
882
+ console.print("[yellow]Full filesystem access granted[/yellow]\n")
883
+ return True
884
+ return False
885
+
798
886
  def _process_single_tool_call(self, tool_call, thinking_indicator):
799
887
  """Process a single tool call.
800
888
 
@@ -826,7 +914,7 @@ class AgenticOrchestrator:
826
914
  panel_to_use = SubAgentPanel(query, self.console)
827
915
 
828
916
  # Execute via tool registry
829
- from tools.base import ToolRegistry, build_context
917
+ from tools.helpers.base import ToolRegistry, build_context
830
918
 
831
919
  tool = ToolRegistry.get(function_name)
832
920
  if tool:
@@ -916,6 +1004,20 @@ class AgenticOrchestrator:
916
1004
  if policy == TERMINAL_YIELD and thinking_indicator:
917
1005
  thinking_indicator.resume()
918
1006
 
1007
+ # Boundary escalation: if the tool result is a path boundary
1008
+ # violation, prompt the user to grant session-wide access.
1009
+ result_str = str(result)
1010
+ if is_boundary_error(result_str):
1011
+ path_arg = arguments.get("path", arguments.get("path_str", ""))
1012
+ if not path_arg:
1013
+ path_arg = extract_boundary_path(result_str)
1014
+ granted = self._boundary_prompt(path_arg)
1015
+ if granted:
1016
+ set_full_filesystem_access(True)
1017
+ # Retry with the boundary now lifted
1018
+ result = tool.execute(arguments, context)
1019
+ result_str = str(result)
1020
+
919
1021
  # Display result for registry tools
920
1022
  # Skip display for tools that take over the terminal (they handle their own display)
921
1023
  if policy != TERMINAL_YIELD:
@@ -948,7 +1050,7 @@ class AgenticOrchestrator:
948
1050
  # Then display feedback
949
1051
  display_tool_feedback(label, result, console, indent=self.is_sub_agent, panel_updater=self.panel_updater)
950
1052
 
951
- return False, str(result)
1053
+ return False, result_str
952
1054
  except Exception as e:
953
1055
  # If thinking_indicator was paused (TERMINAL_YIELD) and tool
954
1056
  # raised, resume it so the spinner reappears for the next iteration
@@ -981,5 +1083,3 @@ def agentic_answer(chat_manager, user_input, console, repo_root, rg_exe_path, de
981
1083
  debug_mode=debug_mode,
982
1084
  )
983
1085
  orchestrator.run(user_input, thinking_indicator)
984
-
985
-
@@ -9,8 +9,9 @@ import requests
9
9
  from typing import Optional, IO
10
10
 
11
11
  from llm.client import LLMClient
12
- from llm.config import get_providers, get_provider_config, reload_config
12
+ from llm.config import get_providers, get_provider_config, get_provider_display_name, reload_config
13
13
  from llm.prompts import build_system_prompt
14
+ from core.skills import render_active_skills_section
14
15
  from pathlib import Path
15
16
  from llm.token_tracker import TokenTracker
16
17
  from utils.settings import server_settings, context_settings
@@ -45,6 +46,10 @@ class ChatManager:
45
46
  self.task_list = []
46
47
  self.task_list_title = None
47
48
 
49
+ # In-session active skill tracking. These skills are rendered into the
50
+ # system prompt for the current chat.
51
+ self.loaded_skills = set()
52
+
48
53
  # .gitignore filtering state
49
54
  self._gitignore_spec = None
50
55
  self._gitignore_mtime = None
@@ -94,6 +99,9 @@ class ChatManager:
94
99
  if self.markdown_logger:
95
100
  self.markdown_logger.start_session()
96
101
 
102
+ # Active skills are scoped to the current message history/session.
103
+ self.loaded_skills = set()
104
+
97
105
  # Start with system prompt only
98
106
  self.messages = [{"role": "system", "content": self._build_system_prompt()}]
99
107
 
@@ -133,7 +141,8 @@ class ChatManager:
133
141
  if variant is None:
134
142
  from utils.settings import prompt_settings
135
143
  variant = prompt_settings.variant
136
- return build_system_prompt(variant)
144
+ active_skills_section = render_active_skills_section(self.loaded_skills)
145
+ return build_system_prompt(variant, active_skills_section=active_skills_section)
137
146
 
138
147
  def update_system_prompt(self, variant: str | None = None):
139
148
  """Rebuild system prompt in-place (e.g. after hotswap or session reset).
@@ -1326,7 +1335,8 @@ Provide a concise summary (2-4 paragraphs) that captures all essential context f
1326
1335
  """
1327
1336
  providers = get_providers()
1328
1337
  if provider_name not in providers:
1329
- return f"Invalid provider. Use /provider to list. Available: {', '.join(providers)}"
1338
+ available = ', '.join(get_provider_display_name(provider) for provider in providers)
1339
+ return f"Invalid provider. Use /provider to list. Available: {available}"
1330
1340
 
1331
1341
  previous_provider = self.client.provider
1332
1342
 
@@ -1342,10 +1352,13 @@ Provide a concise summary (2-4 paragraphs) that captures all essential context f
1342
1352
  # Failed to start server - revert
1343
1353
  self.client.switch_provider(previous_provider)
1344
1354
  self._init_messages(reset_costs=True)
1345
- return f"Failed to start local server. Reverted to {previous_provider} provider."
1355
+ previous_label = get_provider_display_name(previous_provider)
1356
+ return f"Failed to start local server. Reverted to {previous_label} provider."
1346
1357
  self.server_process = server
1347
- return f"Switched to {provider_name} provider (server ready)."
1348
- return f"Switched to {provider_name} provider."
1358
+ provider_label = get_provider_display_name(provider_name)
1359
+ return f"Switched to {provider_label} provider (server ready)."
1360
+ provider_label = get_provider_display_name(provider_name)
1361
+ return f"Switched to {provider_label} provider."
1349
1362
  return "Provider switch failed."
1350
1363
 
1351
1364
  def reload_config(self):
@@ -36,12 +36,16 @@ class ConfigManager:
36
36
  with open(self.config_path, 'r', encoding='utf-8-sig') as f:
37
37
  self._cached_data = yaml.safe_load(f) or {}
38
38
 
39
- # Migrate: rename old provider IDs -> bone (provider ID rename)
39
+ # Migrate legacy provider IDs to current names.
40
40
  old_provider = self._cached_data.get('LAST_PROVIDER')
41
41
  if old_provider in ('vmcode_proxy', 'vmcode_free', 'vmcode'):
42
42
  logger.info("Migrating provider name '%s' -> 'bone'", old_provider)
43
43
  self._cached_data['LAST_PROVIDER'] = 'bone'
44
44
  self.save(self._cached_data, create_backup=True)
45
+ elif old_provider == 'codex_plan':
46
+ logger.info("Migrating provider name '%s' -> 'codex'", old_provider)
47
+ self._cached_data['LAST_PROVIDER'] = 'codex'
48
+ self.save(self._cached_data, create_backup=True)
45
49
 
46
50
  return self._cached_data
47
51
  except yaml.YAMLError as e:
@@ -114,6 +118,7 @@ class ConfigManager:
114
118
  if model is None:
115
119
  provider_model_map = {
116
120
  'bone': 'BONE_PROXY_MODEL',
121
+ 'codex': 'CODEX_PLAN_MODEL',
117
122
  'openrouter': 'OPENROUTER_MODEL',
118
123
  'glm': 'GLM_MODEL',
119
124
  'glm_plan': 'GLM_PLAN_MODEL',
@@ -144,6 +149,7 @@ class ConfigManager:
144
149
  provider_keys = {
145
150
  'local': 'LOCAL_MODEL_PATH',
146
151
  'bone': 'BONE_PROXY_MODEL',
152
+ 'codex': 'CODEX_PLAN_MODEL',
147
153
  'openrouter': 'OPENROUTER_MODEL',
148
154
  'glm': 'GLM_MODEL',
149
155
  'glm_plan': 'GLM_PLAN_MODEL',
@@ -175,6 +181,7 @@ class ConfigManager:
175
181
  provider_keys = {
176
182
  'openrouter': 'OPENROUTER_API_KEY',
177
183
  'bone': 'BONE_PROXY_API_KEY',
184
+ 'codex': 'CODEX_PLAN_API_KEY',
178
185
  'glm': 'GLM_API_KEY',
179
186
  'glm_plan': 'GLM_PLAN_API_KEY',
180
187
  'openai': 'OPENAI_API_KEY',
package/src/core/cron.py CHANGED
@@ -344,15 +344,11 @@ def run_single_job(job: CronJob, console=None, interactive=False) -> None:
344
344
  from core.chat_manager import ChatManager
345
345
  from core.agentic import AgenticOrchestrator
346
346
  from utils.paths import RG_EXE_PATH
347
- from tools.loader import load_all_tools
348
347
  from llm.config import TOOLS_ENABLED
349
348
 
350
349
  if not TOOLS_ENABLED:
351
350
  raise RuntimeError("Cron requires tools to be enabled")
352
351
 
353
- # Ensure tools are loaded
354
- load_all_tools()
355
-
356
352
  # Fresh ChatManager for this job
357
353
  chat_manager = ChatManager()
358
354