bareagent-cli 0.1.0__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.
- bareagent/__init__.py +10 -0
- bareagent/concurrency/__init__.py +6 -0
- bareagent/concurrency/background.py +97 -0
- bareagent/concurrency/notification.py +61 -0
- bareagent/concurrency/scheduler.py +136 -0
- bareagent/config.toml +299 -0
- bareagent/core/__init__.py +1 -0
- bareagent/core/config_paths.py +49 -0
- bareagent/core/context.py +127 -0
- bareagent/core/fileutil.py +103 -0
- bareagent/core/goal.py +214 -0
- bareagent/core/handlers/__init__.py +1 -0
- bareagent/core/handlers/bash.py +79 -0
- bareagent/core/handlers/file_edit.py +47 -0
- bareagent/core/handlers/file_read.py +270 -0
- bareagent/core/handlers/file_write.py +34 -0
- bareagent/core/handlers/glob_search.py +30 -0
- bareagent/core/handlers/goal.py +60 -0
- bareagent/core/handlers/grep_search.py +52 -0
- bareagent/core/handlers/memory.py +71 -0
- bareagent/core/handlers/plan.py +106 -0
- bareagent/core/handlers/search_utils.py +77 -0
- bareagent/core/handlers/skill.py +87 -0
- bareagent/core/handlers/subagent_send.py +70 -0
- bareagent/core/handlers/web_fetch.py +126 -0
- bareagent/core/handlers/web_search.py +165 -0
- bareagent/core/handlers/workflow.py +190 -0
- bareagent/core/loop.py +535 -0
- bareagent/core/retry.py +131 -0
- bareagent/core/sandbox.py +27 -0
- bareagent/core/schema.py +21 -0
- bareagent/core/tools.py +779 -0
- bareagent/core/workflow.py +517 -0
- bareagent/core/workflow_registry.py +219 -0
- bareagent/debug/__init__.py +0 -0
- bareagent/debug/interaction_log.py +263 -0
- bareagent/debug/viewer.html +1750 -0
- bareagent/debug/web_viewer.py +157 -0
- bareagent/hooks/__init__.py +32 -0
- bareagent/hooks/config.py +118 -0
- bareagent/hooks/engine.py +197 -0
- bareagent/hooks/errors.py +14 -0
- bareagent/hooks/events.py +22 -0
- bareagent/lsp/__init__.py +63 -0
- bareagent/lsp/config.py +134 -0
- bareagent/lsp/coord.py +118 -0
- bareagent/lsp/diagnostics.py +240 -0
- bareagent/lsp/errors.py +24 -0
- bareagent/lsp/manager.py +866 -0
- bareagent/lsp/tools.py +629 -0
- bareagent/lsp/workspace_edit.py +305 -0
- bareagent/main.py +4205 -0
- bareagent/mcp/__init__.py +69 -0
- bareagent/mcp/_sse.py +69 -0
- bareagent/mcp/client.py +341 -0
- bareagent/mcp/config.py +169 -0
- bareagent/mcp/errors.py +32 -0
- bareagent/mcp/manager.py +318 -0
- bareagent/mcp/protocol.py +187 -0
- bareagent/mcp/registry.py +557 -0
- bareagent/mcp/transport/__init__.py +15 -0
- bareagent/mcp/transport/base.py +149 -0
- bareagent/mcp/transport/http_legacy.py +192 -0
- bareagent/mcp/transport/http_streamable.py +217 -0
- bareagent/mcp/transport/stdio.py +202 -0
- bareagent/memory/__init__.py +1 -0
- bareagent/memory/compact.py +203 -0
- bareagent/memory/conversation_io.py +226 -0
- bareagent/memory/embedding.py +194 -0
- bareagent/memory/persistent.py +515 -0
- bareagent/memory/token_counter.py +67 -0
- bareagent/memory/token_tracker.py +262 -0
- bareagent/memory/transcript.py +100 -0
- bareagent/permission/__init__.py +1 -0
- bareagent/permission/guard.py +329 -0
- bareagent/permission/rules.py +19 -0
- bareagent/planning/__init__.py +19 -0
- bareagent/planning/agent_types.py +169 -0
- bareagent/planning/skill_gen.py +141 -0
- bareagent/planning/skill_store.py +173 -0
- bareagent/planning/skills.py +146 -0
- bareagent/planning/subagent.py +355 -0
- bareagent/planning/subagent_registry.py +77 -0
- bareagent/planning/tasks.py +348 -0
- bareagent/planning/todo.py +153 -0
- bareagent/planning/worktree.py +122 -0
- bareagent/provider/__init__.py +1 -0
- bareagent/provider/anthropic.py +348 -0
- bareagent/provider/base.py +136 -0
- bareagent/provider/factory.py +130 -0
- bareagent/provider/openai.py +881 -0
- bareagent/provider/presets.py +72 -0
- bareagent/provider/setup.py +356 -0
- bareagent/skills/.gitkeep +1 -0
- bareagent/skills/code-review/SKILL.md +68 -0
- bareagent/skills/git/SKILL.md +68 -0
- bareagent/skills/test/SKILL.md +70 -0
- bareagent/team/__init__.py +17 -0
- bareagent/team/autonomous.py +193 -0
- bareagent/team/mailbox.py +239 -0
- bareagent/team/manager.py +155 -0
- bareagent/team/protocols.py +129 -0
- bareagent/tracing/__init__.py +12 -0
- bareagent/tracing/_api.py +92 -0
- bareagent/tracing/_proxy.py +60 -0
- bareagent/tracing/composite.py +115 -0
- bareagent/tracing/json_file.py +115 -0
- bareagent/tracing/langfuse.py +139 -0
- bareagent/tracing/otel.py +107 -0
- bareagent/tracing/setup.py +85 -0
- bareagent/ui/__init__.py +24 -0
- bareagent/ui/console.py +167 -0
- bareagent/ui/prompt.py +78 -0
- bareagent/ui/protocol.py +24 -0
- bareagent/ui/stream.py +66 -0
- bareagent/ui/theme.py +240 -0
- bareagent_cli-0.1.0.dist-info/METADATA +331 -0
- bareagent_cli-0.1.0.dist-info/RECORD +121 -0
- bareagent_cli-0.1.0.dist-info/WHEEL +4 -0
- bareagent_cli-0.1.0.dist-info/entry_points.txt +2 -0
- bareagent_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Literal
|
|
5
|
+
|
|
6
|
+
ProviderRoute = Literal["anthropic", "openai"]
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(slots=True, frozen=True)
|
|
10
|
+
class ProviderPreset:
|
|
11
|
+
"""Static configuration for a known provider channel.
|
|
12
|
+
|
|
13
|
+
Drives both the factory (route + default base_url/key env) and, later, the
|
|
14
|
+
interactive setup wizard (display_name + candidate_models). Plain immutable
|
|
15
|
+
data, looked up by id via :func:`resolve_preset` -- no dynamic registry.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
id: str
|
|
19
|
+
display_name: str
|
|
20
|
+
route: ProviderRoute
|
|
21
|
+
default_base_url: str | None
|
|
22
|
+
default_api_key_env: str
|
|
23
|
+
candidate_models: tuple[str, ...] = field(default_factory=tuple)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
PROVIDER_PRESETS: dict[str, ProviderPreset] = {
|
|
27
|
+
"anthropic": ProviderPreset(
|
|
28
|
+
id="anthropic",
|
|
29
|
+
display_name="Claude (Anthropic)",
|
|
30
|
+
route="anthropic",
|
|
31
|
+
default_base_url=None,
|
|
32
|
+
default_api_key_env="ANTHROPIC_API_KEY",
|
|
33
|
+
candidate_models=("claude-sonnet-4-20250514", "claude-opus-4-20250514"),
|
|
34
|
+
),
|
|
35
|
+
"openai": ProviderPreset(
|
|
36
|
+
id="openai",
|
|
37
|
+
display_name="ChatGPT (OpenAI)",
|
|
38
|
+
route="openai",
|
|
39
|
+
default_base_url=None,
|
|
40
|
+
default_api_key_env="OPENAI_API_KEY",
|
|
41
|
+
candidate_models=("gpt-4.1", "gpt-4o"),
|
|
42
|
+
),
|
|
43
|
+
"deepseek": ProviderPreset(
|
|
44
|
+
id="deepseek",
|
|
45
|
+
display_name="DeepSeek",
|
|
46
|
+
route="openai",
|
|
47
|
+
default_base_url="https://api.deepseek.com",
|
|
48
|
+
default_api_key_env="DEEPSEEK_API_KEY",
|
|
49
|
+
candidate_models=("deepseek-chat", "deepseek-reasoner"),
|
|
50
|
+
),
|
|
51
|
+
"qwen": ProviderPreset(
|
|
52
|
+
id="qwen",
|
|
53
|
+
display_name="Qwen (DashScope)",
|
|
54
|
+
route="openai",
|
|
55
|
+
default_base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
|
|
56
|
+
default_api_key_env="DASHSCOPE_API_KEY",
|
|
57
|
+
candidate_models=("qwen-plus", "qwen-max", "qwen-turbo"),
|
|
58
|
+
),
|
|
59
|
+
"glm": ProviderPreset(
|
|
60
|
+
id="glm",
|
|
61
|
+
display_name="GLM (Zhipu/BigModel)",
|
|
62
|
+
route="openai",
|
|
63
|
+
default_base_url="https://open.bigmodel.cn/api/paas/v4",
|
|
64
|
+
default_api_key_env="ZHIPUAI_API_KEY",
|
|
65
|
+
candidate_models=("glm-4.6", "glm-4-plus"),
|
|
66
|
+
),
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def resolve_preset(preset_id: str) -> ProviderPreset | None:
|
|
71
|
+
"""Look up a provider preset by id (case-insensitive), or None if unknown."""
|
|
72
|
+
return PROVIDER_PRESETS.get(preset_id.strip().lower())
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
"""Interactive ``bareagent init`` setup wizard + stdlib-only TOML writer.
|
|
2
|
+
|
|
3
|
+
This module owns "choosing and configuring a provider", so per
|
|
4
|
+
``directory-structure.md`` it lives next to ``presets.py`` / ``factory.py``
|
|
5
|
+
rather than in a new top-level package. The wizard is a numbered-menu + line
|
|
6
|
+
input flow (no full-screen prompt-toolkit dialog -- simpler, cross-platform,
|
|
7
|
+
testable). IO is injected via ``input_fn`` / ``output_fn`` so tests can script
|
|
8
|
+
answers without touching real stdin.
|
|
9
|
+
|
|
10
|
+
Writing is stdlib-only: read the existing ``config.local.toml`` text, replace
|
|
11
|
+
or insert just the ``[provider]`` section, validate the result with
|
|
12
|
+
``tomllib``, then atomically write it back -- no ``tomlkit`` / ``tomli``
|
|
13
|
+
dependency (see ``quality-guidelines.md``).
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import json
|
|
19
|
+
import tomllib
|
|
20
|
+
from collections.abc import Callable
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
from bareagent.core.config_paths import local_config_path
|
|
24
|
+
from bareagent.core.fileutil import atomic_write_text
|
|
25
|
+
from bareagent.provider.presets import PROVIDER_PRESETS, ProviderPreset
|
|
26
|
+
|
|
27
|
+
InputFn = Callable[[str], str]
|
|
28
|
+
OutputFn = Callable[[str], None]
|
|
29
|
+
|
|
30
|
+
# Ordered list of channels shown in the menu. The first five are presets; the
|
|
31
|
+
# sixth is the custom OpenAI-compatible branch.
|
|
32
|
+
_MENU_PRESET_IDS = ("deepseek", "openai", "anthropic", "qwen", "glm")
|
|
33
|
+
_CUSTOM_CHOICE_LABEL = "Third-party OpenAI-compatible (custom base_url)"
|
|
34
|
+
|
|
35
|
+
# Field order used when rendering the [provider] table so the written file is
|
|
36
|
+
# stable and readable.
|
|
37
|
+
_PROVIDER_FIELD_ORDER = (
|
|
38
|
+
"name",
|
|
39
|
+
"model",
|
|
40
|
+
"base_url",
|
|
41
|
+
"api_key",
|
|
42
|
+
"api_key_env",
|
|
43
|
+
"wire_api",
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def run_setup_wizard(
|
|
48
|
+
*,
|
|
49
|
+
config_path: Path,
|
|
50
|
+
input_fn: InputFn | None = None,
|
|
51
|
+
output_fn: OutputFn | None = None,
|
|
52
|
+
) -> bool:
|
|
53
|
+
"""Run the interactive provider setup wizard.
|
|
54
|
+
|
|
55
|
+
Collects a channel choice, model, base_url and API key, then writes the
|
|
56
|
+
``[provider]`` section into the ``.local`` sibling of *config_path*,
|
|
57
|
+
preserving every other section. Returns ``True`` when the config was
|
|
58
|
+
written, ``False`` when the user cancelled or gave unrecoverable input.
|
|
59
|
+
|
|
60
|
+
IO is injectable: *input_fn* defaults to :func:`input` and *output_fn* to
|
|
61
|
+
:func:`print`, so tests drive it with scripted answers and captured output.
|
|
62
|
+
"""
|
|
63
|
+
ask = input_fn if input_fn is not None else input
|
|
64
|
+
say = output_fn if output_fn is not None else print
|
|
65
|
+
|
|
66
|
+
try:
|
|
67
|
+
choice = _select_channel(ask, say)
|
|
68
|
+
except _WizardCancelled:
|
|
69
|
+
say("Setup cancelled.")
|
|
70
|
+
return False
|
|
71
|
+
if choice is None:
|
|
72
|
+
return False
|
|
73
|
+
|
|
74
|
+
if choice == "custom":
|
|
75
|
+
table = _collect_custom(ask, say)
|
|
76
|
+
else:
|
|
77
|
+
table = _collect_preset(choice, ask, say)
|
|
78
|
+
if table is None:
|
|
79
|
+
return False
|
|
80
|
+
|
|
81
|
+
local_path = _local_config_path(config_path)
|
|
82
|
+
try:
|
|
83
|
+
_write_provider_section(local_path, table)
|
|
84
|
+
except OSError as exc:
|
|
85
|
+
say(f"Failed to write {local_path}: {exc}")
|
|
86
|
+
return False
|
|
87
|
+
except tomllib.TOMLDecodeError as exc:
|
|
88
|
+
# The in-memory validation in _write_provider_section rejected the
|
|
89
|
+
# spliced result (e.g. the existing file has an oddly-placed `[provider]`
|
|
90
|
+
# line inside a multi-line string). Abort cleanly -- the original file is
|
|
91
|
+
# never touched because validation runs before the atomic write.
|
|
92
|
+
say(f"Refusing to write {local_path}: result would not be valid TOML ({exc}).")
|
|
93
|
+
return False
|
|
94
|
+
|
|
95
|
+
say("")
|
|
96
|
+
say(f"Provider configuration written to {local_path}.")
|
|
97
|
+
say("Run `bareagent` to start a session with the configured channel.")
|
|
98
|
+
if "api_key_env" in table:
|
|
99
|
+
say(
|
|
100
|
+
"Remember to export the environment variable "
|
|
101
|
+
f"{table['api_key_env']} before running BareAgent."
|
|
102
|
+
)
|
|
103
|
+
return True
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class _WizardCancelled(Exception):
|
|
107
|
+
"""Internal signal that the user aborted the wizard (EOF / Ctrl+C)."""
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _ask(ask: InputFn, prompt: str) -> str:
|
|
111
|
+
try:
|
|
112
|
+
return ask(prompt).strip()
|
|
113
|
+
except (EOFError, KeyboardInterrupt) as exc:
|
|
114
|
+
raise _WizardCancelled from exc
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _select_channel(ask: InputFn, say: OutputFn) -> str | None:
|
|
118
|
+
"""Prompt for a channel; return a preset id, ``"custom"``, or ``None``."""
|
|
119
|
+
say("BareAgent provider setup")
|
|
120
|
+
say("Select a provider channel:")
|
|
121
|
+
for index, preset_id in enumerate(_MENU_PRESET_IDS, start=1):
|
|
122
|
+
preset = PROVIDER_PRESETS[preset_id]
|
|
123
|
+
say(f" {index}) {preset.display_name}")
|
|
124
|
+
custom_index = len(_MENU_PRESET_IDS) + 1
|
|
125
|
+
say(f" {custom_index}) {_CUSTOM_CHOICE_LABEL}")
|
|
126
|
+
|
|
127
|
+
raw = _ask(ask, f"Channel [1-{custom_index}]: ")
|
|
128
|
+
if not raw:
|
|
129
|
+
say("No channel selected.")
|
|
130
|
+
return None
|
|
131
|
+
try:
|
|
132
|
+
selected = int(raw)
|
|
133
|
+
except ValueError:
|
|
134
|
+
say(f"Invalid choice: {raw!r}. Expected a number 1-{custom_index}.")
|
|
135
|
+
return None
|
|
136
|
+
if selected == custom_index:
|
|
137
|
+
return "custom"
|
|
138
|
+
if 1 <= selected <= len(_MENU_PRESET_IDS):
|
|
139
|
+
return _MENU_PRESET_IDS[selected - 1]
|
|
140
|
+
say(f"Choice out of range: {selected}. Expected 1-{custom_index}.")
|
|
141
|
+
return None
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _collect_preset(
|
|
145
|
+
preset_id: str,
|
|
146
|
+
ask: InputFn,
|
|
147
|
+
say: OutputFn,
|
|
148
|
+
) -> dict[str, str] | None:
|
|
149
|
+
preset = PROVIDER_PRESETS[preset_id]
|
|
150
|
+
say("")
|
|
151
|
+
say(f"Configuring {preset.display_name}.")
|
|
152
|
+
|
|
153
|
+
model = _prompt_model(preset, ask, say)
|
|
154
|
+
if model is None:
|
|
155
|
+
return None
|
|
156
|
+
|
|
157
|
+
base_url = _prompt_base_url(preset.default_base_url, ask, say, required=False)
|
|
158
|
+
|
|
159
|
+
table: dict[str, str] = {"name": preset.id, "model": model}
|
|
160
|
+
if base_url:
|
|
161
|
+
table["base_url"] = base_url
|
|
162
|
+
|
|
163
|
+
if not _apply_key(table, preset.default_api_key_env, ask, say):
|
|
164
|
+
return None
|
|
165
|
+
return table
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _collect_custom(ask: InputFn, say: OutputFn) -> dict[str, str] | None:
|
|
169
|
+
say("")
|
|
170
|
+
say("Configuring a third-party OpenAI-compatible channel.")
|
|
171
|
+
|
|
172
|
+
name = _ask(ask, "Provider route name [openai]: ") or "openai"
|
|
173
|
+
base_url = _prompt_base_url(None, ask, say, required=True)
|
|
174
|
+
if base_url is None:
|
|
175
|
+
return None
|
|
176
|
+
model = _ask(ask, "Model: ")
|
|
177
|
+
if not model:
|
|
178
|
+
say("Model is required.")
|
|
179
|
+
return None
|
|
180
|
+
|
|
181
|
+
table: dict[str, str] = {"name": name, "model": model, "base_url": base_url}
|
|
182
|
+
if not _apply_key(table, "OPENAI_API_KEY", ask, say):
|
|
183
|
+
return None
|
|
184
|
+
return table
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _prompt_model(
|
|
188
|
+
preset: ProviderPreset,
|
|
189
|
+
ask: InputFn,
|
|
190
|
+
say: OutputFn,
|
|
191
|
+
) -> str | None:
|
|
192
|
+
candidates = preset.candidate_models
|
|
193
|
+
if not candidates:
|
|
194
|
+
model = _ask(ask, "Model: ")
|
|
195
|
+
if not model:
|
|
196
|
+
say("Model is required.")
|
|
197
|
+
return None
|
|
198
|
+
return model
|
|
199
|
+
|
|
200
|
+
say("Candidate models:")
|
|
201
|
+
for index, name in enumerate(candidates, start=1):
|
|
202
|
+
say(f" {index}) {name}")
|
|
203
|
+
raw = _ask(
|
|
204
|
+
ask,
|
|
205
|
+
f"Model [number 1-{len(candidates)}, or type a custom name, default {candidates[0]}]: ",
|
|
206
|
+
)
|
|
207
|
+
if not raw:
|
|
208
|
+
return candidates[0]
|
|
209
|
+
try:
|
|
210
|
+
selected = int(raw)
|
|
211
|
+
except ValueError:
|
|
212
|
+
return raw
|
|
213
|
+
if 1 <= selected <= len(candidates):
|
|
214
|
+
return candidates[selected - 1]
|
|
215
|
+
say(f"Choice out of range: {selected}. Using {candidates[0]}.")
|
|
216
|
+
return candidates[0]
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _prompt_base_url(
|
|
220
|
+
default: str | None,
|
|
221
|
+
ask: InputFn,
|
|
222
|
+
say: OutputFn,
|
|
223
|
+
*,
|
|
224
|
+
required: bool,
|
|
225
|
+
) -> str | None:
|
|
226
|
+
if default:
|
|
227
|
+
value = _ask(ask, f"Base URL [{default}]: ")
|
|
228
|
+
return value or default
|
|
229
|
+
value = _ask(ask, "Base URL: ")
|
|
230
|
+
if value:
|
|
231
|
+
return value
|
|
232
|
+
if required:
|
|
233
|
+
say("Base URL is required for a custom OpenAI-compatible channel.")
|
|
234
|
+
return None
|
|
235
|
+
return ""
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _apply_key(
|
|
239
|
+
table: dict[str, str],
|
|
240
|
+
default_api_key_env: str,
|
|
241
|
+
ask: InputFn,
|
|
242
|
+
say: OutputFn,
|
|
243
|
+
) -> bool:
|
|
244
|
+
"""Collect the key into *table* as ``api_key`` or ``api_key_env``.
|
|
245
|
+
|
|
246
|
+
Returns ``True`` on success, ``False`` when required input was missing.
|
|
247
|
+
"""
|
|
248
|
+
say("API key storage:")
|
|
249
|
+
say(" 1) Write the key in plaintext to config.local.toml (default)")
|
|
250
|
+
say(" 2) Use an environment variable instead")
|
|
251
|
+
storage = _ask(ask, "Choice [1-2, default 1]: ")
|
|
252
|
+
if storage == "2":
|
|
253
|
+
env_name = _ask(ask, f"Environment variable name [{default_api_key_env}]: ")
|
|
254
|
+
table["api_key_env"] = env_name or default_api_key_env
|
|
255
|
+
return True
|
|
256
|
+
|
|
257
|
+
api_key = _ask(ask, "API key: ")
|
|
258
|
+
if not api_key:
|
|
259
|
+
say("API key is required.")
|
|
260
|
+
return False
|
|
261
|
+
table["api_key"] = api_key
|
|
262
|
+
return True
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def _local_config_path(config_path: Path) -> Path:
|
|
266
|
+
"""Return the writable ``config.local.toml`` path for *config_path*.
|
|
267
|
+
|
|
268
|
+
Delegates to :func:`bareagent.core.config_paths.local_config_path` so the
|
|
269
|
+
wizard writes exactly the file that ``load_config`` later merges as the local
|
|
270
|
+
override layer: the CWD for the bundled default, or the ``.local`` sibling for
|
|
271
|
+
an explicit ``--config`` path.
|
|
272
|
+
"""
|
|
273
|
+
return local_config_path(config_path)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _render_provider_section(provider_table: dict[str, str]) -> str:
|
|
277
|
+
"""Render the ``[provider]`` table as a TOML text block (no trailing NL)."""
|
|
278
|
+
lines = ["[provider]"]
|
|
279
|
+
rendered_keys: set[str] = set()
|
|
280
|
+
for key in _PROVIDER_FIELD_ORDER:
|
|
281
|
+
value = provider_table.get(key)
|
|
282
|
+
if not value:
|
|
283
|
+
continue
|
|
284
|
+
lines.append(f"{key} = {json.dumps(value, ensure_ascii=False)}")
|
|
285
|
+
rendered_keys.add(key)
|
|
286
|
+
# Any extra simple keys (defensive: should not normally happen) keep a
|
|
287
|
+
# deterministic order so the file stays stable.
|
|
288
|
+
for key in sorted(provider_table):
|
|
289
|
+
if key in rendered_keys:
|
|
290
|
+
continue
|
|
291
|
+
value = provider_table[key]
|
|
292
|
+
if not value:
|
|
293
|
+
continue
|
|
294
|
+
lines.append(f"{key} = {json.dumps(value, ensure_ascii=False)}")
|
|
295
|
+
return "\n".join(lines)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def _replace_provider_block(original: str, new_block: str) -> str:
|
|
299
|
+
"""Replace or append the top-level ``[provider]`` section in *original*.
|
|
300
|
+
|
|
301
|
+
Only the exact top-level table header ``[provider]`` is matched -- not
|
|
302
|
+
``[provider.xxx]`` sub-tables and not ``[[provider]]`` arrays. The section
|
|
303
|
+
spans from its header line up to (but excluding) the next top-level
|
|
304
|
+
``[``-prefixed header or EOF.
|
|
305
|
+
"""
|
|
306
|
+
lines = original.splitlines()
|
|
307
|
+
start = _find_provider_header(lines)
|
|
308
|
+
new_lines = new_block.splitlines()
|
|
309
|
+
|
|
310
|
+
if start is None:
|
|
311
|
+
return _append_provider_block(original, new_block)
|
|
312
|
+
|
|
313
|
+
end = len(lines)
|
|
314
|
+
for index in range(start + 1, len(lines)):
|
|
315
|
+
if lines[index].lstrip().startswith("["):
|
|
316
|
+
end = index
|
|
317
|
+
break
|
|
318
|
+
|
|
319
|
+
rebuilt = lines[:start] + new_lines + lines[end:]
|
|
320
|
+
text = "\n".join(rebuilt)
|
|
321
|
+
if original.endswith("\n") or not original:
|
|
322
|
+
text += "\n"
|
|
323
|
+
return text
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def _find_provider_header(lines: list[str]) -> int | None:
|
|
327
|
+
for index, line in enumerate(lines):
|
|
328
|
+
if line.strip() == "[provider]":
|
|
329
|
+
return index
|
|
330
|
+
return None
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def _append_provider_block(original: str, new_block: str) -> str:
|
|
334
|
+
if not original.strip():
|
|
335
|
+
return new_block + "\n"
|
|
336
|
+
separator = "" if original.endswith("\n") else "\n"
|
|
337
|
+
# Blank line before the appended section keeps the file readable.
|
|
338
|
+
return f"{original}{separator}\n{new_block}\n"
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def _write_provider_section(config_path: Path, provider_table: dict[str, str]) -> None:
|
|
342
|
+
"""Replace/insert the ``[provider]`` section in *config_path*, atomically.
|
|
343
|
+
|
|
344
|
+
Preserves every other section verbatim. The resulting text is validated
|
|
345
|
+
with ``tomllib`` *before* it is written, so a malformed result raises
|
|
346
|
+
instead of corrupting the file.
|
|
347
|
+
"""
|
|
348
|
+
original = config_path.read_text(encoding="utf-8") if config_path.is_file() else ""
|
|
349
|
+
new_block = _render_provider_section(provider_table)
|
|
350
|
+
updated = _replace_provider_block(original, new_block)
|
|
351
|
+
|
|
352
|
+
# Validate in memory before touching disk -- a parse failure must abort
|
|
353
|
+
# rather than write a broken file.
|
|
354
|
+
tomllib.loads(updated)
|
|
355
|
+
|
|
356
|
+
atomic_write_text(config_path, updated)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Code Review
|
|
2
|
+
|
|
3
|
+
Use this skill when you need a practical review checklist for correctness, safety, performance, and maintainability.
|
|
4
|
+
|
|
5
|
+
## Review Order
|
|
6
|
+
|
|
7
|
+
- Start with behavior and risk.
|
|
8
|
+
- Then inspect security-sensitive edges.
|
|
9
|
+
- Then inspect performance hotspots.
|
|
10
|
+
- Finish with readability and tests.
|
|
11
|
+
|
|
12
|
+
## Correctness
|
|
13
|
+
|
|
14
|
+
- Does the change satisfy the stated task?
|
|
15
|
+
- Are error cases handled deliberately?
|
|
16
|
+
- Are assumptions validated or documented?
|
|
17
|
+
- Are return values consistent across branches?
|
|
18
|
+
- Are default values safe?
|
|
19
|
+
- Is state updated in the right order?
|
|
20
|
+
- Can stale data survive after an update?
|
|
21
|
+
- Are identifiers stable and deterministic when tests need them?
|
|
22
|
+
|
|
23
|
+
## Security
|
|
24
|
+
|
|
25
|
+
- Check command execution inputs for injection risk.
|
|
26
|
+
- Check file path handling for traversal or escape risk.
|
|
27
|
+
- Check network calls for accidental secret leakage.
|
|
28
|
+
- Check tool outputs for overexposure of sensitive data.
|
|
29
|
+
- Check whether permissions are bypassed unintentionally.
|
|
30
|
+
- Check whether untrusted input reaches shell commands directly.
|
|
31
|
+
|
|
32
|
+
## Performance
|
|
33
|
+
|
|
34
|
+
- Look for repeated full-file scans in hot paths.
|
|
35
|
+
- Check whether expensive work can be cached safely.
|
|
36
|
+
- Avoid unnecessary large prompt injections.
|
|
37
|
+
- Prefer on-demand loading for heavy reference content.
|
|
38
|
+
- Keep loops linear unless a stronger structure is required.
|
|
39
|
+
- Watch for duplicate subprocess calls.
|
|
40
|
+
|
|
41
|
+
## Readability
|
|
42
|
+
|
|
43
|
+
- Prefer explicit names over clever abstractions.
|
|
44
|
+
- Keep handlers thin and delegate logic to modules.
|
|
45
|
+
- Make formatting output easy for the agent to parse.
|
|
46
|
+
- Avoid hidden side effects in helper functions.
|
|
47
|
+
- Keep validation errors specific.
|
|
48
|
+
|
|
49
|
+
## Testing
|
|
50
|
+
|
|
51
|
+
- Add tests for the happy path.
|
|
52
|
+
- Add tests for invalid input where behavior matters.
|
|
53
|
+
- Add tests for regressions around integration seams.
|
|
54
|
+
- Prefer deterministic outputs that are easy to assert.
|
|
55
|
+
- Verify new tools are reachable from the registry path.
|
|
56
|
+
|
|
57
|
+
## Review Findings Format
|
|
58
|
+
|
|
59
|
+
- Report the most severe issue first.
|
|
60
|
+
- Include file and line when possible.
|
|
61
|
+
- Explain the concrete failure mode.
|
|
62
|
+
- Suggest the smallest correction that closes the gap.
|
|
63
|
+
|
|
64
|
+
## When No Findings Exist
|
|
65
|
+
|
|
66
|
+
- Say so explicitly.
|
|
67
|
+
- Note any remaining test gaps or assumptions.
|
|
68
|
+
- Separate confirmed behavior from inferred behavior.
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Git Workflow
|
|
2
|
+
|
|
3
|
+
Use this skill when you need to prepare commits, choose a branch name, or check whether a change is ready to land.
|
|
4
|
+
|
|
5
|
+
## Goals
|
|
6
|
+
|
|
7
|
+
- Keep history readable.
|
|
8
|
+
- Make branch purpose obvious.
|
|
9
|
+
- Prefer small, reviewable commits.
|
|
10
|
+
- Avoid mixing unrelated changes.
|
|
11
|
+
|
|
12
|
+
## Branch Naming
|
|
13
|
+
|
|
14
|
+
- Use lowercase words and hyphens only.
|
|
15
|
+
- Prefer `feature/<area>-<summary>` for new work.
|
|
16
|
+
- Prefer `fix/<area>-<summary>` for bug fixes.
|
|
17
|
+
- Prefer `chore/<area>-<summary>` for maintenance.
|
|
18
|
+
- Prefer `docs/<area>-<summary>` for documentation-only work.
|
|
19
|
+
- Keep names short enough to read in `git branch`.
|
|
20
|
+
- Encode the user-facing intent, not the implementation detail.
|
|
21
|
+
- Good: `feature/planning-skills-loader`
|
|
22
|
+
- Good: `fix/repl-nag-reminder`
|
|
23
|
+
- Bad: `misc/stuff`
|
|
24
|
+
- Bad: `feature/final-version-v2`
|
|
25
|
+
|
|
26
|
+
## Commit Structure
|
|
27
|
+
|
|
28
|
+
- Follow Conventional Commits.
|
|
29
|
+
- Format: `<type>(<scope>): <summary>`
|
|
30
|
+
- Omit the scope if it adds no value.
|
|
31
|
+
- Keep the summary in imperative mood.
|
|
32
|
+
- Keep the summary under 72 characters when practical.
|
|
33
|
+
- Types:
|
|
34
|
+
- `feat`: user-visible capability
|
|
35
|
+
- `fix`: behavior correction
|
|
36
|
+
- `refactor`: internal code change without behavior change
|
|
37
|
+
- `test`: test-only change
|
|
38
|
+
- `docs`: documentation-only change
|
|
39
|
+
- `chore`: maintenance or tooling
|
|
40
|
+
|
|
41
|
+
## Commit Message Examples
|
|
42
|
+
|
|
43
|
+
- `feat(planning): add in-memory todo manager`
|
|
44
|
+
- `fix(main): inject nag reminder before latest user turn`
|
|
45
|
+
- `test(skills): cover skill scanning and loading`
|
|
46
|
+
- `docs(readme): explain planning layer`
|
|
47
|
+
|
|
48
|
+
## Commit Hygiene
|
|
49
|
+
|
|
50
|
+
- Review `git diff --stat` before committing.
|
|
51
|
+
- Stage only files related to one logical change.
|
|
52
|
+
- Separate refactors from behavior changes when possible.
|
|
53
|
+
- Do not include generated files unless required by the repo.
|
|
54
|
+
- Mention follow-up work in the body, not the summary.
|
|
55
|
+
|
|
56
|
+
## Before Commit
|
|
57
|
+
|
|
58
|
+
- Run the narrowest relevant test command first.
|
|
59
|
+
- Re-read changed error messages and user-facing text.
|
|
60
|
+
- Check for accidental debug prints.
|
|
61
|
+
- Check for unrelated formatting churn.
|
|
62
|
+
- Confirm new files are included.
|
|
63
|
+
|
|
64
|
+
## When Unsure
|
|
65
|
+
|
|
66
|
+
- Prefer two small commits over one mixed commit.
|
|
67
|
+
- Prefer a boring branch name over a clever one.
|
|
68
|
+
- Ask whether the commit title would still make sense in six months.
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Testing
|
|
2
|
+
|
|
3
|
+
Use this skill when writing or reviewing tests for agent loops, tool handlers, prompt assembly, or filesystem behavior.
|
|
4
|
+
|
|
5
|
+
## Core Principles
|
|
6
|
+
|
|
7
|
+
- Test behavior, not implementation trivia.
|
|
8
|
+
- Keep tests deterministic.
|
|
9
|
+
- Prefer local fixtures over shared global state.
|
|
10
|
+
- Make failure messages easy to understand.
|
|
11
|
+
|
|
12
|
+
## AAA Structure
|
|
13
|
+
|
|
14
|
+
- Arrange the minimal state needed.
|
|
15
|
+
- Act with one clear trigger.
|
|
16
|
+
- Assert the smallest useful surface.
|
|
17
|
+
- Split cases instead of hiding multiple expectations in one test.
|
|
18
|
+
|
|
19
|
+
## What To Cover
|
|
20
|
+
|
|
21
|
+
- Happy path behavior.
|
|
22
|
+
- Validation failures that users or the agent can trigger.
|
|
23
|
+
- Ordering-sensitive outputs when they affect prompt quality.
|
|
24
|
+
- Integration seams where objects are wired together.
|
|
25
|
+
- Empty-state behavior.
|
|
26
|
+
- State transitions across multiple calls.
|
|
27
|
+
|
|
28
|
+
## Boundary Conditions
|
|
29
|
+
|
|
30
|
+
- Zero items.
|
|
31
|
+
- One item.
|
|
32
|
+
- Multiple items.
|
|
33
|
+
- Missing required fields.
|
|
34
|
+
- Unknown IDs or names.
|
|
35
|
+
- Invalid enum-like values.
|
|
36
|
+
- Whitespace-only inputs when relevant.
|
|
37
|
+
|
|
38
|
+
## Mock Strategy
|
|
39
|
+
|
|
40
|
+
- Mock provider calls at the boundary, not deep inside data objects.
|
|
41
|
+
- Use simple fake classes when behavior is small and stateful.
|
|
42
|
+
- Prefer monkeypatch for subprocess or environment access.
|
|
43
|
+
- Avoid mocking code you can exercise cheaply for real.
|
|
44
|
+
|
|
45
|
+
## Assertions
|
|
46
|
+
|
|
47
|
+
- Assert exact text when prompt wording matters.
|
|
48
|
+
- Assert substrings when the surrounding text is intentionally flexible.
|
|
49
|
+
- Assert ordering when the agent depends on ordered lists.
|
|
50
|
+
- Do not assert incidental whitespace unless required.
|
|
51
|
+
|
|
52
|
+
## Filesystem Tests
|
|
53
|
+
|
|
54
|
+
- Use `tmp_path` for skill directories and generated files.
|
|
55
|
+
- Keep paths relative to the temp workspace.
|
|
56
|
+
- Write UTF-8 text explicitly.
|
|
57
|
+
|
|
58
|
+
## Review Checklist
|
|
59
|
+
|
|
60
|
+
- Can this test fail for the right reason?
|
|
61
|
+
- Does it break if the intended behavior regresses?
|
|
62
|
+
- Is there a simpler fixture setup?
|
|
63
|
+
- Is the name specific about the scenario?
|
|
64
|
+
|
|
65
|
+
## Anti-Patterns
|
|
66
|
+
|
|
67
|
+
- One test covering multiple unrelated behaviors.
|
|
68
|
+
- Mocking the method under test.
|
|
69
|
+
- Asserting internal counters unless they are part of behavior.
|
|
70
|
+
- Requiring network or real API keys for unit tests.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Team collaboration modules for BareAgent."""
|
|
2
|
+
|
|
3
|
+
from bareagent.team.autonomous import AutonomousAgent
|
|
4
|
+
from bareagent.team.mailbox import Message, MessageBus
|
|
5
|
+
from bareagent.team.manager import AgentInstance, Teammate, TeammateManager
|
|
6
|
+
from bareagent.team.protocols import Protocol, ProtocolFSM
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"AgentInstance",
|
|
10
|
+
"AutonomousAgent",
|
|
11
|
+
"Message",
|
|
12
|
+
"MessageBus",
|
|
13
|
+
"Protocol",
|
|
14
|
+
"ProtocolFSM",
|
|
15
|
+
"Teammate",
|
|
16
|
+
"TeammateManager",
|
|
17
|
+
]
|