sanityops-cli 0.1.3__py3-none-any.whl
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.
- sanityops_cli/__init__.py +16 -0
- sanityops_cli/agents/__init__.py +14 -0
- sanityops_cli/agents/repair_agent/__init__.py +19 -0
- sanityops_cli/agents/repair_agent/agent.py +230 -0
- sanityops_cli/agents/repair_agent/prompts.py +100 -0
- sanityops_cli/agents/repair_agent/tools/__init__.py +19 -0
- sanityops_cli/agents/repair_agent/tools/store_repairs_tool.py +144 -0
- sanityops_cli/agents/scanner_agent/agent.py +332 -0
- sanityops_cli/agents/scanner_agent/hooks/progress_hook.py +152 -0
- sanityops_cli/agents/scanner_agent/models/finding.py +120 -0
- sanityops_cli/agents/scanner_agent/prompts.py +316 -0
- sanityops_cli/agents/scanner_agent/tools/grep_tool.py +667 -0
- sanityops_cli/agents/scanner_agent/tools/listfiles_tool.py +88 -0
- sanityops_cli/agents/scanner_agent/tools/readfile_tool.py +804 -0
- sanityops_cli/agents/scanner_agent/tools/storefindings_tool.py +178 -0
- sanityops_cli/api/__init__.py +16 -0
- sanityops_cli/api/client.py +403 -0
- sanityops_cli/commands/__init__.py +14 -0
- sanityops_cli/commands/config.py +312 -0
- sanityops_cli/commands/init.py +132 -0
- sanityops_cli/commands/inspect.py +646 -0
- sanityops_cli/constants/__init__.py +14 -0
- sanityops_cli/constants/config_defaults.py +22 -0
- sanityops_cli/constants/exit_codes.py +19 -0
- sanityops_cli/defect_checker/__init__.py +16 -0
- sanityops_cli/defect_checker/checker.py +100 -0
- sanityops_cli/defect_checker/llm_config.py +112 -0
- sanityops_cli/defect_checker/markdown_reporter.py +249 -0
- sanityops_cli/defect_checker/renderer.py +203 -0
- sanityops_cli/exceptions/__init__.py +14 -0
- sanityops_cli/exceptions/api_exceptions.py +60 -0
- sanityops_cli/exceptions/base_exceptions.py +25 -0
- sanityops_cli/help_panel.py +49 -0
- sanityops_cli/logging/__init__.py +18 -0
- sanityops_cli/logging/logger.py +108 -0
- sanityops_cli/main.py +123 -0
- sanityops_cli/progress/__init__.py +18 -0
- sanityops_cli/progress/tracker.py +159 -0
- sanityops_cli/renderers/__init__.py +14 -0
- sanityops_cli/renderers/command_renderer/inspect_command_renderer.py +87 -0
- sanityops_cli/templates/__init__.py +14 -0
- sanityops_cli/templates/inspect_config.yaml +55 -0
- sanityops_cli/utils/__init__.py +14 -0
- sanityops_cli/utils/artifact_packer.py +407 -0
- sanityops_cli/utils/config_loader.py +296 -0
- sanityops_cli/utils/config_resolver.py +358 -0
- sanityops_cli/utils/validators.py +117 -0
- sanityops_cli-0.1.3.dist-info/METADATA +213 -0
- sanityops_cli-0.1.3.dist-info/RECORD +53 -0
- sanityops_cli-0.1.3.dist-info/WHEEL +4 -0
- sanityops_cli-0.1.3.dist-info/entry_points.txt +2 -0
- sanityops_cli-0.1.3.dist-info/licenses/LICENSE +201 -0
- sanityops_cli-0.1.3.dist-info/licenses/NOTICE +5 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Copyright 2026 zipsonken
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
#
|
|
15
|
+
|
|
16
|
+
__version__ = "0.1.3"
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Copyright 2026 zipsonken
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
#
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Copyright 2026 zipsonken
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""Repair agent for generating fixed artifact content from inspection reports."""
|
|
16
|
+
|
|
17
|
+
from .agent import RepairAgent
|
|
18
|
+
|
|
19
|
+
__all__ = ["RepairAgent"]
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
# Copyright 2026 zipsonken
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""Repair agent: generate repaired artifact content from a local defect report."""
|
|
16
|
+
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import TYPE_CHECKING
|
|
19
|
+
|
|
20
|
+
import anyio
|
|
21
|
+
from rich.console import Console
|
|
22
|
+
|
|
23
|
+
from sanityops_cli.exceptions.base_exceptions import ValidationError
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from sanityops_cli.logging.logger import Logger
|
|
27
|
+
|
|
28
|
+
# ============================================================================
|
|
29
|
+
# RepairAgent Class
|
|
30
|
+
# ============================================================================
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class RepairAgent:
|
|
34
|
+
"""Runs an LLM agent that rewrites defective artifacts using the report.
|
|
35
|
+
|
|
36
|
+
Mirrors ScannerAgent: builds a sanityops_agent parent agent with a tool
|
|
37
|
+
registry (read + store_repair), feeds it the inspection report plus the
|
|
38
|
+
artifact paths, and collects repairs via the shared-memory
|
|
39
|
+
StoreRepairsTool.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(
|
|
43
|
+
self,
|
|
44
|
+
provider,
|
|
45
|
+
max_loops: int = 30,
|
|
46
|
+
timeout: int = 300,
|
|
47
|
+
token_budget: int | None = None,
|
|
48
|
+
llm_retries: int = 3,
|
|
49
|
+
retry_delay: float = 65.0,
|
|
50
|
+
verbose: bool = False,
|
|
51
|
+
console: Console | None = None,
|
|
52
|
+
logger: "Logger | None" = None,
|
|
53
|
+
):
|
|
54
|
+
self.provider = provider
|
|
55
|
+
self.max_loops = max_loops
|
|
56
|
+
self.timeout = timeout
|
|
57
|
+
self.token_budget = token_budget
|
|
58
|
+
self.verbose = verbose
|
|
59
|
+
self.console = console or Console()
|
|
60
|
+
self.logger = logger
|
|
61
|
+
# Rate-limit resilience: the server-side LLM proxy may cap requests
|
|
62
|
+
# far below what one repair run needs (e.g. rpm_limit=1). The SDK's
|
|
63
|
+
# own retry backs off only 1-2s, so we retry the whole run here with
|
|
64
|
+
# a delay long enough to clear a per-minute quota.
|
|
65
|
+
self.llm_retries = llm_retries
|
|
66
|
+
self.retry_delay = retry_delay
|
|
67
|
+
|
|
68
|
+
async def repair(
|
|
69
|
+
self,
|
|
70
|
+
report_path: str,
|
|
71
|
+
artifacts: dict[str, list[str]],
|
|
72
|
+
project_root: str,
|
|
73
|
+
) -> list[dict]:
|
|
74
|
+
"""Repair artifacts listed in the report.
|
|
75
|
+
|
|
76
|
+
Retries the whole run on rate-limit errors (nothing to lose: repairs
|
|
77
|
+
only materialize after store_repair, and a rate-limited run usually
|
|
78
|
+
dies before storing anything).
|
|
79
|
+
"""
|
|
80
|
+
last_error: Exception | None = None
|
|
81
|
+
for attempt in range(self.llm_retries + 1):
|
|
82
|
+
try:
|
|
83
|
+
return await self._run_once(report_path, artifacts, project_root)
|
|
84
|
+
except ValidationError as e:
|
|
85
|
+
last_error = e
|
|
86
|
+
if attempt >= self.llm_retries or not _is_rate_limit_error(e):
|
|
87
|
+
raise
|
|
88
|
+
self.console.print(
|
|
89
|
+
f" [yellow]⚠[/] LLM rate limited — waiting {self.retry_delay:.0f}s "
|
|
90
|
+
f"before retry {attempt + 1}/{self.llm_retries}..."
|
|
91
|
+
)
|
|
92
|
+
await anyio.sleep(self.retry_delay)
|
|
93
|
+
assert last_error is not None
|
|
94
|
+
raise last_error
|
|
95
|
+
|
|
96
|
+
async def _run_once(
|
|
97
|
+
self,
|
|
98
|
+
report_path: str,
|
|
99
|
+
artifacts: dict[str, list[str]],
|
|
100
|
+
project_root: str,
|
|
101
|
+
) -> list[dict]:
|
|
102
|
+
"""Single repair attempt (see repair() for the retry wrapper)."""
|
|
103
|
+
from sanityops_agent.agents import AgentFactory
|
|
104
|
+
from sanityops_agent.core.agent import AgentConfig, TerminationReason
|
|
105
|
+
from sanityops_agent.hooks.base import HookExecutor
|
|
106
|
+
from sanityops_agent.tools import ToolRegistry
|
|
107
|
+
|
|
108
|
+
from sanityops_cli.agents.repair_agent.prompts import REPAIR_AGENT_PROMPT
|
|
109
|
+
from sanityops_cli.agents.repair_agent.tools.store_repairs_tool import StoreRepairsTool
|
|
110
|
+
from sanityops_cli.agents.scanner_agent.tools.readfile_tool import FileReadTool
|
|
111
|
+
|
|
112
|
+
report = Path(report_path).resolve()
|
|
113
|
+
if not report.is_file():
|
|
114
|
+
raise ValidationError(f"Report not found: {report_path}")
|
|
115
|
+
|
|
116
|
+
all_paths = (
|
|
117
|
+
artifacts.get("prompts", []) + artifacts.get("tools", []) + artifacts.get("skills", [])
|
|
118
|
+
)
|
|
119
|
+
if not all_paths:
|
|
120
|
+
raise ValidationError("No artifacts configured to repair")
|
|
121
|
+
|
|
122
|
+
for p in all_paths:
|
|
123
|
+
path = Path(p)
|
|
124
|
+
if not path.is_absolute():
|
|
125
|
+
raise ValidationError(f"Artifact path must be absolute: {p}")
|
|
126
|
+
if not path.exists():
|
|
127
|
+
raise ValidationError(f"Artifact path does not exist: {p}")
|
|
128
|
+
|
|
129
|
+
# Deliberately NO TaskTool: repair must follow the artifact list
|
|
130
|
+
# exactly, not spawn sub-agents to explore the project. Registering it
|
|
131
|
+
# led agents to wander (listing directories, reading bogus paths) and
|
|
132
|
+
# burn the whole token budget without storing a single repair.
|
|
133
|
+
registry = ToolRegistry()
|
|
134
|
+
registry.register(FileReadTool())
|
|
135
|
+
registry.register(StoreRepairsTool())
|
|
136
|
+
StoreRepairsTool.clear()
|
|
137
|
+
|
|
138
|
+
artifact_lines: list[str] = []
|
|
139
|
+
for kind, key in (("prompt", "prompts"), ("tool", "tools"), ("skill", "skills")):
|
|
140
|
+
for p in artifacts.get(key) or []:
|
|
141
|
+
artifact_lines.append(f"- type: {kind}, path: {p}")
|
|
142
|
+
|
|
143
|
+
system_prompt = REPAIR_AGENT_PROMPT.format(
|
|
144
|
+
report_path=str(report),
|
|
145
|
+
project_root=project_root,
|
|
146
|
+
artifact_list="\n".join(artifact_lines),
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
config_kwargs = {
|
|
150
|
+
"max_loops": self.max_loops,
|
|
151
|
+
"total_timeout": self.timeout,
|
|
152
|
+
"system_prompt": system_prompt,
|
|
153
|
+
}
|
|
154
|
+
if self.token_budget:
|
|
155
|
+
config_kwargs["token_budget"] = self.token_budget
|
|
156
|
+
|
|
157
|
+
config = AgentConfig(**config_kwargs)
|
|
158
|
+
|
|
159
|
+
hook_executor = HookExecutor()
|
|
160
|
+
if self.verbose and self.logger:
|
|
161
|
+
from sanityops_cli.agents.scanner_agent.hooks.progress_hook import ProgressHook
|
|
162
|
+
hook_executor.register(
|
|
163
|
+
ProgressHook(self.console, verbose=self.verbose, logger=self.logger)
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
factory = AgentFactory(
|
|
167
|
+
provider=self.provider,
|
|
168
|
+
config=config,
|
|
169
|
+
tool_registry=registry,
|
|
170
|
+
hook_executor=hook_executor,
|
|
171
|
+
)
|
|
172
|
+
agent = factory.create_parent_agent(system_prompt=system_prompt)
|
|
173
|
+
|
|
174
|
+
if self.verbose:
|
|
175
|
+
self.console.print()
|
|
176
|
+
|
|
177
|
+
result = await agent.run(f"Repair artifacts listed in report {report}")
|
|
178
|
+
|
|
179
|
+
if self.verbose:
|
|
180
|
+
self.console.print()
|
|
181
|
+
|
|
182
|
+
repairs = StoreRepairsTool.get_repairs()
|
|
183
|
+
if result.termination_reason != TerminationReason.END_TURN:
|
|
184
|
+
# Any termination after repairs were already stored is a partial
|
|
185
|
+
# success: return collected repairs with a warning instead of
|
|
186
|
+
# discarding them (budget/loop exhaustion or a mid-run LLM error).
|
|
187
|
+
if repairs:
|
|
188
|
+
if self.verbose:
|
|
189
|
+
self.console.print(
|
|
190
|
+
f" [yellow]⚠[/] Agent stopped ({result.termination_reason.value}) "
|
|
191
|
+
f"after {len(repairs)} repair(s) were collected"
|
|
192
|
+
)
|
|
193
|
+
return repairs
|
|
194
|
+
error_msg = result.error or f"Agent terminated: {result.termination_reason.value}"
|
|
195
|
+
if result.error_detail:
|
|
196
|
+
error_detail_str = "\n".join(
|
|
197
|
+
f" - {e.error_type}: {e.message}" for e in result.error_detail
|
|
198
|
+
)
|
|
199
|
+
error_msg += f"\nError Detail:\n{error_detail_str}"
|
|
200
|
+
raise ValidationError(f"Agent execution failed: {error_msg}")
|
|
201
|
+
|
|
202
|
+
return repairs
|
|
203
|
+
|
|
204
|
+
def repair_sync(
|
|
205
|
+
self,
|
|
206
|
+
report_path: str,
|
|
207
|
+
artifacts: dict[str, list[str]],
|
|
208
|
+
project_root: str,
|
|
209
|
+
) -> list[dict]:
|
|
210
|
+
"""Synchronous wrapper around repair()."""
|
|
211
|
+
return anyio.run(self.repair, report_path, artifacts, project_root)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _is_rate_limit_error(exc: BaseException) -> bool:
|
|
215
|
+
"""Best-effort detection of a server-side rate limit / throttling error.
|
|
216
|
+
|
|
217
|
+
The LLM proxy (litellm) surfaces these as 429 with `throttling_error`, or
|
|
218
|
+
as 5xx wrappers whose message still mentions the rate limit. Matching on
|
|
219
|
+
the text markers keeps this working across provider SDKs.
|
|
220
|
+
"""
|
|
221
|
+
text = str(exc).lower()
|
|
222
|
+
markers = (
|
|
223
|
+
"429",
|
|
224
|
+
"rate limit",
|
|
225
|
+
"ratelimiterror",
|
|
226
|
+
"throttling_error",
|
|
227
|
+
"too many requests",
|
|
228
|
+
"no deployments available",
|
|
229
|
+
)
|
|
230
|
+
return any(marker in text for marker in markers)
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# Copyright 2026 zipsonken
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""System prompt for the repair agent."""
|
|
16
|
+
|
|
17
|
+
REPAIR_AGENT_PROMPT = """\
|
|
18
|
+
You are an artifact repair agent. Your job is to fix the defects reported in an
|
|
19
|
+
inspection report by rewriting the defective artifacts.
|
|
20
|
+
|
|
21
|
+
## Inputs
|
|
22
|
+
|
|
23
|
+
- `{report_path}`: Absolute path to the inspection report (markdown). It lists
|
|
24
|
+
every defect found in the artifacts, grouped per artifact, with severity
|
|
25
|
+
(P0/P1/P2), description, location, impact, and a suggested fix.
|
|
26
|
+
- Artifacts live under the project root `{project_root}`. Paths referenced in
|
|
27
|
+
the report (or listed below) are relative to it unless absolute.
|
|
28
|
+
|
|
29
|
+
## Artifacts Under Repair
|
|
30
|
+
|
|
31
|
+
The following artifacts were inspected and may require repair:
|
|
32
|
+
{artifact_list}
|
|
33
|
+
|
|
34
|
+
## Workflow (follow strictly)
|
|
35
|
+
|
|
36
|
+
1. Read the inspection report at `{report_path}` using the `read` tool.
|
|
37
|
+
2. Identify every artifact that has at least one defect in the report.
|
|
38
|
+
Artifacts whose section says "No defects found" must be skipped entirely.
|
|
39
|
+
3. For each defective artifact, in this order:
|
|
40
|
+
a. Read the artifact source file with the `read` tool — use the EXACT
|
|
41
|
+
absolute path from the list above. Never guess, never explore the
|
|
42
|
+
project, never read paths you have not been given.
|
|
43
|
+
b. Rewrite the FULL artifact content so that all reported defects in that
|
|
44
|
+
artifact are resolved. You must not introduce new defects; keep the
|
|
45
|
+
original structure, language, and unrelated content intact. Apply the
|
|
46
|
+
report's suggested fixes unless they conflict with other content, in
|
|
47
|
+
which case use your judgment to keep the artifact consistent.
|
|
48
|
+
c. Call `store_repair` immediately with:
|
|
49
|
+
- artifact_type: "prompt" | "tool" | "skill"
|
|
50
|
+
- artifact_path: absolute path of the artifact file you read
|
|
51
|
+
- repaired_content: the complete rewritten file content
|
|
52
|
+
- summary: one short paragraph listing which defect IDs you addressed
|
|
53
|
+
4. Cross-module defects (IDs starting with QD-PT / QD-ST, shown in the
|
|
54
|
+
"Cross" section) describe inconsistencies BETWEEN artifacts. Resolve them
|
|
55
|
+
by editing the artifact(s) the fix suggestion targets; a single cross
|
|
56
|
+
defect may require touching two artifacts — store each repaired artifact
|
|
57
|
+
separately.
|
|
58
|
+
|
|
59
|
+
## Output Language
|
|
60
|
+
|
|
61
|
+
- All summaries and repair descriptions must be written in English.
|
|
62
|
+
- Preserve the original language of the artifact content itself (Chinese stays
|
|
63
|
+
Chinese, English stays English). Comments and explanations inside the
|
|
64
|
+
repaired content should be minimal.
|
|
65
|
+
|
|
66
|
+
## Scope discipline (IMPORTANT)
|
|
67
|
+
|
|
68
|
+
You have exactly two tools and one job. Do NOT:
|
|
69
|
+
- explore the project tree, list directories, or glob for other files;
|
|
70
|
+
- read the report more than once;
|
|
71
|
+
- read any path that is not in the Artifacts list above (a failed read
|
|
72
|
+
wastes a full turn and the token budget);
|
|
73
|
+
- rewrite artifacts that have no defects.
|
|
74
|
+
|
|
75
|
+
Every wasted turn can exhaust the budget before a single repair is stored.
|
|
76
|
+
|
|
77
|
+
## Rules
|
|
78
|
+
|
|
79
|
+
- repaired_content MUST be the complete file content, never a diff or snippet.
|
|
80
|
+
- Never delete functionality to make a defect disappear unless the report
|
|
81
|
+
explicitly says so; prefer completing or correcting definitions.
|
|
82
|
+
- If the report contains no defects at all, store nothing and report that.
|
|
83
|
+
- Process artifacts one at a time: read -> repair -> store_repair -> next.
|
|
84
|
+
- Token budget is limited. Be economical: do NOT re-read files you have
|
|
85
|
+
already read; do NOT re-store an artifact you already stored (unless you
|
|
86
|
+
are correcting it); think through the repair BEFORE producing output and
|
|
87
|
+
emit the repaired content in one shot — avoid iterating on drafts.
|
|
88
|
+
|
|
89
|
+
## Available Tools
|
|
90
|
+
|
|
91
|
+
- `read(file_path)`: read a file from disk. `file_path` must be an absolute path.
|
|
92
|
+
- `store_repair(...)`: REQUIRED. Store each repaired artifact exactly as
|
|
93
|
+
specified above.
|
|
94
|
+
|
|
95
|
+
## Termination
|
|
96
|
+
|
|
97
|
+
Finish when every defective artifact has been stored via `store_repair`.
|
|
98
|
+
Then reply with a one-line summary: how many artifacts repaired, how many
|
|
99
|
+
defect IDs addressed.
|
|
100
|
+
"""
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Copyright 2026 zipsonken
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""Repair agent tools."""
|
|
16
|
+
|
|
17
|
+
from .store_repairs_tool import StoreRepairsTool
|
|
18
|
+
|
|
19
|
+
__all__ = ["StoreRepairsTool"]
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# Copyright 2026 zipsonken
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""Shared-memory store for repaired artifacts produced by the repair agent.
|
|
16
|
+
|
|
17
|
+
The agent calls `store_repair` once per defective artifact with the fully
|
|
18
|
+
rewritten content; the CLI flushes everything into a single markdown
|
|
19
|
+
report under `.sanityops/repairs/`.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import json
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import ClassVar
|
|
25
|
+
|
|
26
|
+
from sanityops_agent.tools.base import Tool, ToolResult
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class StoreRepairsTool(Tool):
|
|
30
|
+
"""Shared-memory store for repaired artifacts produced by the repair agent.
|
|
31
|
+
|
|
32
|
+
The agent calls `store_repair` once per defective artifact with the fully
|
|
33
|
+
rewritten content; the CLI flushes everything into a single markdown
|
|
34
|
+
report under `.sanityops/repairs/`.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
name = "store_repair"
|
|
38
|
+
description = "store the repaired content of one artifact"
|
|
39
|
+
parameters = {
|
|
40
|
+
"type": "object",
|
|
41
|
+
"properties": {
|
|
42
|
+
"artifact_type": {
|
|
43
|
+
"type": "string",
|
|
44
|
+
"enum": ["skill", "tool", "prompt"],
|
|
45
|
+
"description": "which kind of artifact was repaired",
|
|
46
|
+
},
|
|
47
|
+
"artifact_path": {
|
|
48
|
+
"type": "string",
|
|
49
|
+
"description": "absolute path of the source artifact file that was repaired",
|
|
50
|
+
},
|
|
51
|
+
"repaired_content": {
|
|
52
|
+
"type": "string",
|
|
53
|
+
"description": "the complete repaired artifact content (full file, not a diff)",
|
|
54
|
+
},
|
|
55
|
+
"summary": {
|
|
56
|
+
"type": "string",
|
|
57
|
+
"description": "one-paragraph summary of the defects addressed",
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
"required": ["artifact_type", "artifact_path", "repaired_content", "summary"],
|
|
61
|
+
}
|
|
62
|
+
tags = ["memory", "repair"]
|
|
63
|
+
|
|
64
|
+
_repairs: ClassVar[list[dict]] = []
|
|
65
|
+
|
|
66
|
+
async def execute(
|
|
67
|
+
self,
|
|
68
|
+
artifact_type: str,
|
|
69
|
+
artifact_path: str,
|
|
70
|
+
repaired_content: str,
|
|
71
|
+
summary: str,
|
|
72
|
+
) -> ToolResult:
|
|
73
|
+
"""Execute the store_repair operation."""
|
|
74
|
+
return self._add_repair(artifact_type, artifact_path, repaired_content, summary)
|
|
75
|
+
|
|
76
|
+
def _add_repair(
|
|
77
|
+
self,
|
|
78
|
+
artifact_type: str,
|
|
79
|
+
artifact_path: str,
|
|
80
|
+
repaired_content: str,
|
|
81
|
+
summary: str,
|
|
82
|
+
) -> ToolResult:
|
|
83
|
+
"""Add a repair entry to the shared store."""
|
|
84
|
+
# Validate artifact_type
|
|
85
|
+
if artifact_type not in ("skill", "tool", "prompt"):
|
|
86
|
+
return ToolResult(
|
|
87
|
+
content="",
|
|
88
|
+
success=False,
|
|
89
|
+
error=f"invalid artifact_type: {artifact_type}",
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
# Validate absolute path
|
|
93
|
+
path = Path(artifact_path)
|
|
94
|
+
if not path.is_absolute():
|
|
95
|
+
return ToolResult(
|
|
96
|
+
content="",
|
|
97
|
+
success=False,
|
|
98
|
+
error=f"artifact_path must be absolute: {artifact_path}",
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
# Validate non-empty content
|
|
102
|
+
if not repaired_content or not repaired_content.strip():
|
|
103
|
+
return ToolResult(
|
|
104
|
+
content="",
|
|
105
|
+
success=False,
|
|
106
|
+
error="repaired_content is empty",
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
# Create entry
|
|
110
|
+
entry = {
|
|
111
|
+
"artifact_type": artifact_type,
|
|
112
|
+
"artifact_path": str(path),
|
|
113
|
+
"repaired_content": repaired_content,
|
|
114
|
+
"summary": summary,
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
# Update existing entry for same artifact, or add new
|
|
118
|
+
for index, existing in enumerate(self._repairs):
|
|
119
|
+
if existing["artifact_path"] == entry["artifact_path"]:
|
|
120
|
+
self._repairs[index] = entry
|
|
121
|
+
action = "updated"
|
|
122
|
+
break
|
|
123
|
+
else:
|
|
124
|
+
self._repairs.append(entry)
|
|
125
|
+
action = "added"
|
|
126
|
+
|
|
127
|
+
return ToolResult(
|
|
128
|
+
content=json.dumps({
|
|
129
|
+
"success": True,
|
|
130
|
+
"message": f"{action} repair for {artifact_type}: {path.name}",
|
|
131
|
+
"count": len(self._repairs),
|
|
132
|
+
}),
|
|
133
|
+
success=True,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
@classmethod
|
|
137
|
+
def get_repairs(cls) -> list[dict]:
|
|
138
|
+
"""Return all stored repairs."""
|
|
139
|
+
return list(cls._repairs)
|
|
140
|
+
|
|
141
|
+
@classmethod
|
|
142
|
+
def clear(cls) -> None:
|
|
143
|
+
"""Clear all stored repairs."""
|
|
144
|
+
cls._repairs.clear()
|