bone-agent 1.4.0 → 2.0.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.
- package/bin/bone.js +39 -0
- package/package.json +25 -39
- package/LICENSE +0 -21
- package/README.md +0 -201
- package/bin/npm-wrapper.js +0 -235
- package/bin/rg +0 -0
- package/bin/rg.exe +0 -0
- package/config.yaml.example +0 -144
- package/prompts/main/ask_questions.md +0 -31
- package/prompts/main/batch_independent_calls.md +0 -5
- package/prompts/main/casual_interactions.md +0 -11
- package/prompts/main/code_references.md +0 -8
- package/prompts/main/communication_style.md +0 -12
- package/prompts/main/context_reliability.md +0 -12
- package/prompts/main/conversational_tool_calling.md +0 -15
- package/prompts/main/dream.md +0 -50
- package/prompts/main/editing_pattern.md +0 -13
- package/prompts/main/error_handling.md +0 -6
- package/prompts/main/exploration_pattern.md +0 -21
- package/prompts/main/intro.md +0 -1
- package/prompts/main/obsidian.md +0 -16
- package/prompts/main/obsidian_project.md +0 -79
- package/prompts/main/professional_objectivity.md +0 -3
- package/prompts/main/skills.md +0 -3
- package/prompts/main/targeted_searching.md +0 -10
- package/prompts/main/task_lists_pattern.md +0 -8
- package/prompts/main/temp_folder.md +0 -9
- package/prompts/main/think_before_acting.md +0 -10
- package/prompts/main/tone_and_style.md +0 -4
- package/prompts/main/tool_preferences.md +0 -24
- package/prompts/main/trust_subagent_context.md +0 -21
- package/prompts/main/when_to_use_sub_agent.md +0 -7
- package/prompts/micro/ask_questions.md +0 -1
- package/prompts/micro/batch_independent_calls.md +0 -1
- package/prompts/micro/casual_interactions.md +0 -1
- package/prompts/micro/code_references.md +0 -1
- package/prompts/micro/communication_style.md +0 -1
- package/prompts/micro/context_reliability.md +0 -1
- package/prompts/micro/conversational_tool_calling.md +0 -1
- package/prompts/micro/editing_pattern.md +0 -1
- package/prompts/micro/error_handling.md +0 -1
- package/prompts/micro/exploration_pattern.md +0 -1
- package/prompts/micro/intro.md +0 -1
- package/prompts/micro/obsidian.md +0 -4
- package/prompts/micro/obsidian_project.md +0 -5
- package/prompts/micro/professional_objectivity.md +0 -1
- package/prompts/micro/skills.md +0 -1
- package/prompts/micro/targeted_searching.md +0 -1
- package/prompts/micro/task_lists_pattern.md +0 -1
- package/prompts/micro/temp_folder.md +0 -1
- package/prompts/micro/think_before_acting.md +0 -5
- package/prompts/micro/tone_and_style.md +0 -1
- package/prompts/micro/tool_preferences.md +0 -1
- package/prompts/micro/trust_subagent_context.md +0 -1
- package/prompts/micro/when_to_use_sub_agent.md +0 -1
- package/requirements.txt +0 -9
- package/src/__init__.py +0 -11
- package/src/core/__init__.py +0 -1
- package/src/core/agentic.py +0 -1085
- package/src/core/chat_manager.py +0 -1577
- package/src/core/config_manager.py +0 -260
- package/src/core/cron.py +0 -578
- package/src/core/cron_allowlist.py +0 -118
- package/src/core/memory.py +0 -145
- package/src/core/metadata.py +0 -75
- package/src/core/retry.py +0 -71
- package/src/core/skills.py +0 -463
- package/src/core/sub_agent.py +0 -376
- package/src/core/tool_approval.py +0 -220
- package/src/core/tool_feedback.py +0 -789
- package/src/exceptions.py +0 -79
- package/src/llm/__init__.py +0 -1
- package/src/llm/client.py +0 -176
- package/src/llm/codex_provider.py +0 -350
- package/src/llm/config.py +0 -536
- package/src/llm/prompts.py +0 -494
- package/src/llm/providers.py +0 -438
- package/src/llm/streaming.py +0 -163
- package/src/llm/token_tracker.py +0 -399
- package/src/tools/__init__.py +0 -151
- package/src/tools/constants.py +0 -59
- package/src/tools/create_file.py +0 -136
- package/src/tools/directory.py +0 -389
- package/src/tools/edit.py +0 -549
- package/src/tools/file_reader.py +0 -322
- package/src/tools/helpers/__init__.py +0 -99
- package/src/tools/helpers/base.py +0 -599
- package/src/tools/helpers/converters.py +0 -44
- package/src/tools/helpers/file_helpers.py +0 -189
- package/src/tools/helpers/formatters.py +0 -411
- package/src/tools/helpers/loader.py +0 -145
- package/src/tools/helpers/parallel_executor.py +0 -231
- package/src/tools/helpers/path_resolver.py +0 -283
- package/src/tools/helpers/plugin_manifest.py +0 -185
- package/src/tools/obsidian.py +0 -96
- package/src/tools/review_sub_agent.py +0 -190
- package/src/tools/rg_search.py +0 -477
- package/src/tools/search_plugins.py +0 -177
- package/src/tools/select_option.py +0 -600
- package/src/tools/shell.py +0 -302
- package/src/tools/sub_agent.py +0 -139
- package/src/tools/task_list.py +0 -269
- package/src/tools/web_search.py +0 -61
- package/src/ui/__init__.py +0 -1
- package/src/ui/banner.py +0 -87
- package/src/ui/commands.py +0 -3131
- package/src/ui/displays.py +0 -239
- package/src/ui/loader.py +0 -284
- package/src/ui/main.py +0 -643
- package/src/ui/prompt_utils.py +0 -113
- package/src/ui/setting_selector.py +0 -590
- package/src/ui/setup_wizard.py +0 -294
- package/src/ui/sub_agent_panel.py +0 -234
- package/src/ui/tool_confirmation.py +0 -226
- package/src/utils/__init__.py +0 -1
- package/src/utils/citation_parser.py +0 -199
- package/src/utils/editor.py +0 -207
- package/src/utils/gitignore_filter.py +0 -149
- package/src/utils/logger.py +0 -254
- package/src/utils/paths.py +0 -30
- package/src/utils/result_parsers.py +0 -108
- package/src/utils/safe_commands.py +0 -243
- package/src/utils/settings.py +0 -195
- package/src/utils/user_message_logger.py +0 -120
- package/src/utils/validation.py +0 -201
- package/src/utils/web_search.py +0 -173
|
@@ -1,185 +0,0 @@
|
|
|
1
|
-
"""Capability manifest for on-demand plugin and skill discovery.
|
|
2
|
-
|
|
3
|
-
Plugin-tier tools are registered here instead of ToolRegistry at import time.
|
|
4
|
-
This keeps plugin schemas out of the LLM context window until explicitly
|
|
5
|
-
activated via the search_plugins core tool.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
import logging
|
|
9
|
-
from dataclasses import dataclass
|
|
10
|
-
from typing import Dict, Iterable, List, Optional
|
|
11
|
-
|
|
12
|
-
from core.skills import (
|
|
13
|
-
SearchCandidate,
|
|
14
|
-
iter_skill_summaries,
|
|
15
|
-
search_candidates,
|
|
16
|
-
)
|
|
17
|
-
from utils.settings import tool_settings
|
|
18
|
-
|
|
19
|
-
from .base import ToolDefinition
|
|
20
|
-
|
|
21
|
-
logger = logging.getLogger(__name__)
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
@dataclass
|
|
25
|
-
class CapabilityMatch:
|
|
26
|
-
kind: str
|
|
27
|
-
name: str
|
|
28
|
-
description: str
|
|
29
|
-
category: str | None = None
|
|
30
|
-
tags: list[str] | None = None
|
|
31
|
-
tool_def: ToolDefinition | None = None
|
|
32
|
-
preview: str | None = None
|
|
33
|
-
activated: bool = False
|
|
34
|
-
already_active: bool = False
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
class PluginManifest:
|
|
38
|
-
"""Index of plugin-tier tools and stored skills for discovery surfaces.
|
|
39
|
-
|
|
40
|
-
Plugin tools are registered here when modules with @tool(tier="plugin")
|
|
41
|
-
are imported. Stored skills are discovered from disk on demand. The
|
|
42
|
-
search_plugins core tool queries this manifest to find and activate or
|
|
43
|
-
load capabilities.
|
|
44
|
-
"""
|
|
45
|
-
|
|
46
|
-
def __init__(self):
|
|
47
|
-
self._plugins: Dict[str, ToolDefinition] = {}
|
|
48
|
-
|
|
49
|
-
def register(self, tool_def: ToolDefinition) -> None:
|
|
50
|
-
"""Register a plugin tool definition.
|
|
51
|
-
|
|
52
|
-
Args:
|
|
53
|
-
tool_def: ToolDefinition with tier="plugin"
|
|
54
|
-
"""
|
|
55
|
-
if tool_def.name in self._plugins:
|
|
56
|
-
logger.warning(
|
|
57
|
-
f"Plugin '{tool_def.name}' is being overwritten. "
|
|
58
|
-
f"Previous: {self._plugins[tool_def.name].handler}, "
|
|
59
|
-
f"New: {tool_def.handler}"
|
|
60
|
-
)
|
|
61
|
-
self._plugins[tool_def.name] = tool_def
|
|
62
|
-
logger.debug(f"Plugin registered in manifest: {tool_def.name}")
|
|
63
|
-
|
|
64
|
-
def get(self, name: str) -> Optional[ToolDefinition]:
|
|
65
|
-
"""Get a plugin tool definition by name.
|
|
66
|
-
|
|
67
|
-
Args:
|
|
68
|
-
name: Plugin tool name
|
|
69
|
-
|
|
70
|
-
Returns:
|
|
71
|
-
ToolDefinition or None if not found
|
|
72
|
-
"""
|
|
73
|
-
return self._plugins.get(name)
|
|
74
|
-
|
|
75
|
-
def get_all(self) -> List[ToolDefinition]:
|
|
76
|
-
"""Get all registered plugin definitions.
|
|
77
|
-
|
|
78
|
-
Returns:
|
|
79
|
-
List of all ToolDefinitions in the manifest
|
|
80
|
-
"""
|
|
81
|
-
return list(self._plugins.values())
|
|
82
|
-
|
|
83
|
-
def _iter_capabilities(self, category: str = None) -> Iterable[CapabilityMatch]:
|
|
84
|
-
"""Yield available plugin and skill capabilities for discovery surfaces."""
|
|
85
|
-
include_plugins = category in (None, "plugin")
|
|
86
|
-
include_skills = category in (None, "skill")
|
|
87
|
-
disabled_tools = set(tool_settings.disabled_tools or [])
|
|
88
|
-
hidden_skills = set(tool_settings.hidden_skills or [])
|
|
89
|
-
|
|
90
|
-
if include_plugins:
|
|
91
|
-
for tool_def in self._plugins.values():
|
|
92
|
-
if tool_def.name in disabled_tools:
|
|
93
|
-
continue
|
|
94
|
-
if category not in (None, "plugin") and tool_def.category != category:
|
|
95
|
-
continue
|
|
96
|
-
yield CapabilityMatch(
|
|
97
|
-
kind="plugin",
|
|
98
|
-
name=tool_def.name,
|
|
99
|
-
description=tool_def.description,
|
|
100
|
-
category=tool_def.category,
|
|
101
|
-
tags=list(tool_def.tags or []),
|
|
102
|
-
tool_def=tool_def,
|
|
103
|
-
)
|
|
104
|
-
|
|
105
|
-
if include_skills:
|
|
106
|
-
for summary in iter_skill_summaries():
|
|
107
|
-
if summary.name in hidden_skills:
|
|
108
|
-
continue
|
|
109
|
-
yield CapabilityMatch(
|
|
110
|
-
kind="skill",
|
|
111
|
-
name=summary.name,
|
|
112
|
-
description=summary.description or summary.preview,
|
|
113
|
-
category="skill",
|
|
114
|
-
tags=summary.tags or ["skill"],
|
|
115
|
-
preview=summary.preview,
|
|
116
|
-
)
|
|
117
|
-
|
|
118
|
-
def search_capabilities(
|
|
119
|
-
self,
|
|
120
|
-
query: str,
|
|
121
|
-
category: str = None,
|
|
122
|
-
max_results: int = 5,
|
|
123
|
-
) -> List[CapabilityMatch]:
|
|
124
|
-
"""Search plugins and skills through one shared discovery path."""
|
|
125
|
-
combined_candidates = [
|
|
126
|
-
SearchCandidate(
|
|
127
|
-
item=capability,
|
|
128
|
-
text=" ".join(
|
|
129
|
-
part
|
|
130
|
-
for part in [
|
|
131
|
-
capability.name,
|
|
132
|
-
capability.description,
|
|
133
|
-
capability.category or "",
|
|
134
|
-
" ".join(capability.tags or []),
|
|
135
|
-
]
|
|
136
|
-
if part
|
|
137
|
-
),
|
|
138
|
-
compact_text="",
|
|
139
|
-
exact_text=capability.name,
|
|
140
|
-
)
|
|
141
|
-
for capability in self._iter_capabilities(category=category)
|
|
142
|
-
]
|
|
143
|
-
combined_matches = search_candidates(
|
|
144
|
-
query,
|
|
145
|
-
combined_candidates,
|
|
146
|
-
max_results=max_results,
|
|
147
|
-
item_key=lambda capability: f"{capability.kind}:{capability.name}",
|
|
148
|
-
)
|
|
149
|
-
return [match.item for match in combined_matches]
|
|
150
|
-
|
|
151
|
-
def list_all_capabilities(self, category: str = None) -> List[CapabilityMatch]:
|
|
152
|
-
"""Return all available capabilities without fuzzy scoring."""
|
|
153
|
-
return list(self._iter_capabilities(category=category))
|
|
154
|
-
|
|
155
|
-
def get_categories(self) -> List[str]:
|
|
156
|
-
"""Get all unique categories in the manifest.
|
|
157
|
-
|
|
158
|
-
Returns:
|
|
159
|
-
Sorted list of category strings
|
|
160
|
-
"""
|
|
161
|
-
categories = {td.category for td in self._plugins.values() if td.category}
|
|
162
|
-
return sorted(categories)
|
|
163
|
-
|
|
164
|
-
def plugin_count(self) -> int:
|
|
165
|
-
"""Get the number of registered plugins.
|
|
166
|
-
|
|
167
|
-
Returns:
|
|
168
|
-
Number of plugins in the manifest
|
|
169
|
-
"""
|
|
170
|
-
return len(self._plugins)
|
|
171
|
-
|
|
172
|
-
def has_plugin(self, name: str) -> bool:
|
|
173
|
-
"""Check if a plugin exists in the manifest.
|
|
174
|
-
|
|
175
|
-
Args:
|
|
176
|
-
name: Plugin tool name
|
|
177
|
-
|
|
178
|
-
Returns:
|
|
179
|
-
True if plugin is in the manifest
|
|
180
|
-
"""
|
|
181
|
-
return name in self._plugins
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
# Singleton instance
|
|
185
|
-
plugin_manifest = PluginManifest()
|
package/src/tools/obsidian.py
DELETED
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
"""Obsidian vault integration utilities.
|
|
2
|
-
|
|
3
|
-
Provides vault session management for project note routing.
|
|
4
|
-
"""
|
|
5
|
-
|
|
6
|
-
import logging
|
|
7
|
-
import os
|
|
8
|
-
from pathlib import Path
|
|
9
|
-
from dataclasses import dataclass
|
|
10
|
-
from typing import Optional
|
|
11
|
-
|
|
12
|
-
logger = logging.getLogger(__name__)
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
@dataclass(frozen=True)
|
|
16
|
-
class VaultSession:
|
|
17
|
-
"""Immutable snapshot of vault pairing state.
|
|
18
|
-
|
|
19
|
-
Built once when the vault is activated (register()) and shared everywhere.
|
|
20
|
-
Eliminates scattered derivation of vault_root, project_folder, and path prefixes.
|
|
21
|
-
|
|
22
|
-
Attributes:
|
|
23
|
-
vault_root: Absolute validated vault root Path.
|
|
24
|
-
project_folder: Absolute project folder (vault_root / project_base / repo_name).
|
|
25
|
-
project_folder_relative: Vault-relative project folder string (e.g. "Dev/myrepo").
|
|
26
|
-
"""
|
|
27
|
-
vault_root: Path
|
|
28
|
-
project_folder: Path
|
|
29
|
-
project_folder_relative: str
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
# Module-level session — set by register(), cleared by unregister()
|
|
33
|
-
_session: Optional[VaultSession] = None
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
def get_vault_session() -> Optional[VaultSession]:
|
|
37
|
-
"""Return the active VaultSession, or None if vault is not paired."""
|
|
38
|
-
return _session
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
def build_vault_session(repo_root: Path = None) -> Optional[VaultSession]:
|
|
42
|
-
"""Build a VaultSession from current settings.
|
|
43
|
-
|
|
44
|
-
Args:
|
|
45
|
-
repo_root: Repository root used to derive project folder name.
|
|
46
|
-
Falls back to os.getcwd() if not provided.
|
|
47
|
-
|
|
48
|
-
Returns:
|
|
49
|
-
VaultSession if vault is configured and valid, None otherwise.
|
|
50
|
-
"""
|
|
51
|
-
from utils.settings import obsidian_settings
|
|
52
|
-
|
|
53
|
-
if not obsidian_settings.is_active():
|
|
54
|
-
return None
|
|
55
|
-
|
|
56
|
-
root = obsidian_settings.vault_path # Already validated by is_active()
|
|
57
|
-
# Re-resolve to absolute (is_active checks existence but returns raw string)
|
|
58
|
-
root = Path(root).resolve()
|
|
59
|
-
|
|
60
|
-
project_base = obsidian_settings.project_base or "Dev"
|
|
61
|
-
repo_name = repo_root.name if repo_root else os.path.basename(os.getcwd())
|
|
62
|
-
if not repo_name:
|
|
63
|
-
return None
|
|
64
|
-
|
|
65
|
-
project_folder = root / project_base / repo_name
|
|
66
|
-
pf_relative = str(project_base) + "/" + repo_name
|
|
67
|
-
|
|
68
|
-
return VaultSession(
|
|
69
|
-
vault_root=root,
|
|
70
|
-
project_folder=project_folder,
|
|
71
|
-
project_folder_relative=pf_relative,
|
|
72
|
-
)
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
def init_session(repo_root: Path = None) -> Optional[VaultSession]:
|
|
76
|
-
"""Build and cache the VaultSession.
|
|
77
|
-
|
|
78
|
-
Should be called once with the actual repo_root when available.
|
|
79
|
-
Falls back to os.getcwd() if repo_root is None (acceptable for prompt
|
|
80
|
-
building before the orchestrator starts).
|
|
81
|
-
|
|
82
|
-
Returns:
|
|
83
|
-
The active VaultSession, or None if vault is not configured.
|
|
84
|
-
"""
|
|
85
|
-
global _session
|
|
86
|
-
session = build_vault_session(repo_root=repo_root)
|
|
87
|
-
if session:
|
|
88
|
-
# Only cache if we have a real repo_root — otherwise the orchestrator
|
|
89
|
-
# will call us again with the correct path and that should win.
|
|
90
|
-
if repo_root is not None or _session is None:
|
|
91
|
-
_session = session
|
|
92
|
-
logger.info(f"Vault session initialized: {session.project_folder_relative}")
|
|
93
|
-
return _session
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
@@ -1,190 +0,0 @@
|
|
|
1
|
-
"""Review sub-agent tool for analyzing git diffs."""
|
|
2
|
-
|
|
3
|
-
import re
|
|
4
|
-
from pathlib import Path
|
|
5
|
-
|
|
6
|
-
from core.sub_agent import run_sub_agent
|
|
7
|
-
from tools.sub_agent import SimplePanelUpdater
|
|
8
|
-
from utils.citation_parser import inject_file_contents
|
|
9
|
-
|
|
10
|
-
# Approximate chars per token (conservative heuristic, avoids tokenizer dependency)
|
|
11
|
-
_CHARS_PER_TOKEN = 4
|
|
12
|
-
|
|
13
|
-
# Max tokens for the diff context (leaves room for agent conversation within budget)
|
|
14
|
-
_MAX_DIFF_TOKENS = 50_000
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
def _estimate_tokens(text: str) -> int:
|
|
18
|
-
"""Rough token estimate based on character count."""
|
|
19
|
-
return len(text) // _CHARS_PER_TOKEN
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def _truncate_diff(diff: str, max_chars: int) -> tuple:
|
|
23
|
-
"""Truncate a diff to fit within a character budget, keeping whole files.
|
|
24
|
-
|
|
25
|
-
Returns:
|
|
26
|
-
Tuple of (truncated_diff, list_of_skipped_file_names)
|
|
27
|
-
"""
|
|
28
|
-
# Split on diff headers: each file starts with "diff --git"
|
|
29
|
-
file_blocks = re.split(r'(?=^diff --git )', diff, flags=re.MULTILINE)
|
|
30
|
-
# First element may be empty if diff starts with "diff --git"
|
|
31
|
-
if file_blocks and file_blocks[0].strip() == '':
|
|
32
|
-
file_blocks = file_blocks[1:]
|
|
33
|
-
|
|
34
|
-
result_blocks = []
|
|
35
|
-
skipped_files = []
|
|
36
|
-
current_chars = 0
|
|
37
|
-
|
|
38
|
-
for block in file_blocks:
|
|
39
|
-
block_chars = len(block)
|
|
40
|
-
if current_chars + block_chars > max_chars:
|
|
41
|
-
# Extract file name from the block header for the skip list
|
|
42
|
-
match = re.search(r'^diff --git a/(.*?) b/', block, re.MULTILINE)
|
|
43
|
-
if match:
|
|
44
|
-
skipped_files.append(match.group(1))
|
|
45
|
-
else:
|
|
46
|
-
result_blocks.append(block)
|
|
47
|
-
current_chars += block_chars
|
|
48
|
-
|
|
49
|
-
truncated = '\n'.join(result_blocks)
|
|
50
|
-
return truncated, skipped_files
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
def _count_binary_files(diff: str) -> int:
|
|
54
|
-
"""Count binary file entries in a diff."""
|
|
55
|
-
return len(re.findall(r'^Binary files ', diff, re.MULTILINE))
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
def _build_review_context(diff: str) -> str:
|
|
59
|
-
"""Build the initial context message for the review sub-agent.
|
|
60
|
-
|
|
61
|
-
Handles truncation and binary file counting.
|
|
62
|
-
|
|
63
|
-
Returns:
|
|
64
|
-
Tuple of (context_string, warning_string_or_None)
|
|
65
|
-
"""
|
|
66
|
-
warning = None
|
|
67
|
-
max_chars = _MAX_DIFF_TOKENS * _CHARS_PER_TOKEN
|
|
68
|
-
|
|
69
|
-
# Check for binary files
|
|
70
|
-
binary_count = _count_binary_files(diff)
|
|
71
|
-
binary_note = ""
|
|
72
|
-
if binary_count > 0:
|
|
73
|
-
binary_note = f"\n\nNote: {binary_count} binary file(s) were skipped in this diff."
|
|
74
|
-
|
|
75
|
-
# Check if truncation is needed
|
|
76
|
-
estimated_tokens = _estimate_tokens(diff)
|
|
77
|
-
if estimated_tokens > _MAX_DIFF_TOKENS:
|
|
78
|
-
truncated_diff, skipped_files = _truncate_diff(diff, max_chars)
|
|
79
|
-
skipped_note = ""
|
|
80
|
-
if skipped_files:
|
|
81
|
-
skipped_note = (
|
|
82
|
-
f"\n\nWARNING: Diff was too large and has been truncated. "
|
|
83
|
-
f"The following {len(skipped_files)} file(s) were omitted from review:\n"
|
|
84
|
-
+ "\n".join(f" - {f}" for f in skipped_files)
|
|
85
|
-
)
|
|
86
|
-
warning = f"Diff truncated: {len(skipped_files)} file(s) omitted."
|
|
87
|
-
context = f"Review this git diff:\n\n{truncated_diff}{binary_note}{skipped_note}"
|
|
88
|
-
else:
|
|
89
|
-
context = f"Review this git diff:\n\n{diff}{binary_note}"
|
|
90
|
-
|
|
91
|
-
return context, warning
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
def review_changes(
|
|
95
|
-
diff_output: str,
|
|
96
|
-
repo_root: Path,
|
|
97
|
-
rg_exe_path: str,
|
|
98
|
-
console,
|
|
99
|
-
chat_manager,
|
|
100
|
-
gitignore_spec=None,
|
|
101
|
-
panel_updater=None,
|
|
102
|
-
user_intent: str = None,
|
|
103
|
-
) -> dict:
|
|
104
|
-
"""Run review sub-agent on a git diff.
|
|
105
|
-
|
|
106
|
-
Args:
|
|
107
|
-
diff_output: Git diff string to review
|
|
108
|
-
repo_root: Repository root directory
|
|
109
|
-
rg_exe_path: Path to rg executable
|
|
110
|
-
console: Rich console for output
|
|
111
|
-
chat_manager: ChatManager instance
|
|
112
|
-
gitignore_spec: PathSpec for .gitignore filtering
|
|
113
|
-
panel_updater: Optional SubAgentPanel for live updates
|
|
114
|
-
user_intent: Optional description of what the user was trying to do
|
|
115
|
-
|
|
116
|
-
Returns:
|
|
117
|
-
Dict with keys:
|
|
118
|
-
display: Clean review text (no injected file contents)
|
|
119
|
-
history: Review text with file contents injected from citations
|
|
120
|
-
"""
|
|
121
|
-
if not diff_output or not isinstance(diff_output, str) or not diff_output.strip():
|
|
122
|
-
result = "No changes to review."
|
|
123
|
-
return {"display": result, "history": result}
|
|
124
|
-
|
|
125
|
-
# Build context (handles truncation and binary file notes)
|
|
126
|
-
review_context, truncation_warning = _build_review_context(diff_output)
|
|
127
|
-
|
|
128
|
-
if truncation_warning:
|
|
129
|
-
console.print(f"[yellow]{truncation_warning}[/yellow]")
|
|
130
|
-
|
|
131
|
-
# Set up panel updater
|
|
132
|
-
if panel_updater is None:
|
|
133
|
-
panel_updater = SimplePanelUpdater(console)
|
|
134
|
-
|
|
135
|
-
# Task query for the review agent
|
|
136
|
-
if user_intent:
|
|
137
|
-
task_query = (
|
|
138
|
-
"Analyze the diff provided above. "
|
|
139
|
-
"For each changed file, use read_file to get surrounding context. "
|
|
140
|
-
"Then provide a structured code review with findings, issues, and suggestions.\n\n"
|
|
141
|
-
f"The user's intent for these changes was: {user_intent}\n"
|
|
142
|
-
"Use this context to better understand the changes and focus your review accordingly."
|
|
143
|
-
)
|
|
144
|
-
else:
|
|
145
|
-
task_query = (
|
|
146
|
-
"Analyze the diff provided above. "
|
|
147
|
-
"For each changed file, use read_file to get surrounding context. "
|
|
148
|
-
"Then provide a structured code review with findings, issues, and suggestions."
|
|
149
|
-
)
|
|
150
|
-
|
|
151
|
-
with panel_updater as panel:
|
|
152
|
-
sub_agent_data = run_sub_agent(
|
|
153
|
-
task_query=task_query,
|
|
154
|
-
repo_root=repo_root,
|
|
155
|
-
rg_exe_path=rg_exe_path,
|
|
156
|
-
console=console,
|
|
157
|
-
panel_updater=panel,
|
|
158
|
-
sub_agent_type="review",
|
|
159
|
-
initial_context=review_context,
|
|
160
|
-
)
|
|
161
|
-
|
|
162
|
-
# Check for errors
|
|
163
|
-
if sub_agent_data.get('error'):
|
|
164
|
-
panel.set_error(sub_agent_data['error'])
|
|
165
|
-
result = f"exit_code=1\n{sub_agent_data['error']}"
|
|
166
|
-
return {"display": result, "history": result}
|
|
167
|
-
|
|
168
|
-
# Track usage
|
|
169
|
-
usage = sub_agent_data.get('usage', {})
|
|
170
|
-
if usage:
|
|
171
|
-
chat_manager.token_tracker.add_usage(usage, model_name=sub_agent_data.get("model", ""))
|
|
172
|
-
panel.set_complete({
|
|
173
|
-
'prompt_tokens': usage.get('prompt_tokens', 0),
|
|
174
|
-
'completion_tokens': usage.get('completion_tokens', 0),
|
|
175
|
-
'total_tokens': usage.get('total_tokens', 0),
|
|
176
|
-
'context_tokens': usage.get('context_tokens', 0),
|
|
177
|
-
})
|
|
178
|
-
|
|
179
|
-
raw_result = sub_agent_data.get('result', '')
|
|
180
|
-
|
|
181
|
-
# If hard limit was exceeded, skip injection and return raw dump for both
|
|
182
|
-
if sub_agent_data.get('hard_limit_exceeded'):
|
|
183
|
-
return {"display": raw_result, "history": raw_result}
|
|
184
|
-
|
|
185
|
-
# Always inject file contents from citations into the history version
|
|
186
|
-
injected_result = inject_file_contents(
|
|
187
|
-
raw_result, repo_root, gitignore_spec, console
|
|
188
|
-
)
|
|
189
|
-
|
|
190
|
-
return {"display": raw_result, "history": injected_result}
|