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.
Files changed (126) hide show
  1. package/bin/bone.js +39 -0
  2. package/package.json +25 -39
  3. package/LICENSE +0 -21
  4. package/README.md +0 -201
  5. package/bin/npm-wrapper.js +0 -235
  6. package/bin/rg +0 -0
  7. package/bin/rg.exe +0 -0
  8. package/config.yaml.example +0 -144
  9. package/prompts/main/ask_questions.md +0 -31
  10. package/prompts/main/batch_independent_calls.md +0 -5
  11. package/prompts/main/casual_interactions.md +0 -11
  12. package/prompts/main/code_references.md +0 -8
  13. package/prompts/main/communication_style.md +0 -12
  14. package/prompts/main/context_reliability.md +0 -12
  15. package/prompts/main/conversational_tool_calling.md +0 -15
  16. package/prompts/main/dream.md +0 -50
  17. package/prompts/main/editing_pattern.md +0 -13
  18. package/prompts/main/error_handling.md +0 -6
  19. package/prompts/main/exploration_pattern.md +0 -21
  20. package/prompts/main/intro.md +0 -1
  21. package/prompts/main/obsidian.md +0 -16
  22. package/prompts/main/obsidian_project.md +0 -79
  23. package/prompts/main/professional_objectivity.md +0 -3
  24. package/prompts/main/skills.md +0 -3
  25. package/prompts/main/targeted_searching.md +0 -10
  26. package/prompts/main/task_lists_pattern.md +0 -8
  27. package/prompts/main/temp_folder.md +0 -9
  28. package/prompts/main/think_before_acting.md +0 -10
  29. package/prompts/main/tone_and_style.md +0 -4
  30. package/prompts/main/tool_preferences.md +0 -24
  31. package/prompts/main/trust_subagent_context.md +0 -21
  32. package/prompts/main/when_to_use_sub_agent.md +0 -7
  33. package/prompts/micro/ask_questions.md +0 -1
  34. package/prompts/micro/batch_independent_calls.md +0 -1
  35. package/prompts/micro/casual_interactions.md +0 -1
  36. package/prompts/micro/code_references.md +0 -1
  37. package/prompts/micro/communication_style.md +0 -1
  38. package/prompts/micro/context_reliability.md +0 -1
  39. package/prompts/micro/conversational_tool_calling.md +0 -1
  40. package/prompts/micro/editing_pattern.md +0 -1
  41. package/prompts/micro/error_handling.md +0 -1
  42. package/prompts/micro/exploration_pattern.md +0 -1
  43. package/prompts/micro/intro.md +0 -1
  44. package/prompts/micro/obsidian.md +0 -4
  45. package/prompts/micro/obsidian_project.md +0 -5
  46. package/prompts/micro/professional_objectivity.md +0 -1
  47. package/prompts/micro/skills.md +0 -1
  48. package/prompts/micro/targeted_searching.md +0 -1
  49. package/prompts/micro/task_lists_pattern.md +0 -1
  50. package/prompts/micro/temp_folder.md +0 -1
  51. package/prompts/micro/think_before_acting.md +0 -5
  52. package/prompts/micro/tone_and_style.md +0 -1
  53. package/prompts/micro/tool_preferences.md +0 -1
  54. package/prompts/micro/trust_subagent_context.md +0 -1
  55. package/prompts/micro/when_to_use_sub_agent.md +0 -1
  56. package/requirements.txt +0 -9
  57. package/src/__init__.py +0 -11
  58. package/src/core/__init__.py +0 -1
  59. package/src/core/agentic.py +0 -1085
  60. package/src/core/chat_manager.py +0 -1577
  61. package/src/core/config_manager.py +0 -260
  62. package/src/core/cron.py +0 -578
  63. package/src/core/cron_allowlist.py +0 -118
  64. package/src/core/memory.py +0 -145
  65. package/src/core/metadata.py +0 -75
  66. package/src/core/retry.py +0 -71
  67. package/src/core/skills.py +0 -463
  68. package/src/core/sub_agent.py +0 -376
  69. package/src/core/tool_approval.py +0 -220
  70. package/src/core/tool_feedback.py +0 -789
  71. package/src/exceptions.py +0 -79
  72. package/src/llm/__init__.py +0 -1
  73. package/src/llm/client.py +0 -176
  74. package/src/llm/codex_provider.py +0 -350
  75. package/src/llm/config.py +0 -536
  76. package/src/llm/prompts.py +0 -494
  77. package/src/llm/providers.py +0 -438
  78. package/src/llm/streaming.py +0 -163
  79. package/src/llm/token_tracker.py +0 -399
  80. package/src/tools/__init__.py +0 -151
  81. package/src/tools/constants.py +0 -59
  82. package/src/tools/create_file.py +0 -136
  83. package/src/tools/directory.py +0 -389
  84. package/src/tools/edit.py +0 -549
  85. package/src/tools/file_reader.py +0 -322
  86. package/src/tools/helpers/__init__.py +0 -99
  87. package/src/tools/helpers/base.py +0 -599
  88. package/src/tools/helpers/converters.py +0 -44
  89. package/src/tools/helpers/file_helpers.py +0 -189
  90. package/src/tools/helpers/formatters.py +0 -411
  91. package/src/tools/helpers/loader.py +0 -145
  92. package/src/tools/helpers/parallel_executor.py +0 -231
  93. package/src/tools/helpers/path_resolver.py +0 -283
  94. package/src/tools/helpers/plugin_manifest.py +0 -185
  95. package/src/tools/obsidian.py +0 -96
  96. package/src/tools/review_sub_agent.py +0 -190
  97. package/src/tools/rg_search.py +0 -477
  98. package/src/tools/search_plugins.py +0 -177
  99. package/src/tools/select_option.py +0 -600
  100. package/src/tools/shell.py +0 -302
  101. package/src/tools/sub_agent.py +0 -139
  102. package/src/tools/task_list.py +0 -269
  103. package/src/tools/web_search.py +0 -61
  104. package/src/ui/__init__.py +0 -1
  105. package/src/ui/banner.py +0 -87
  106. package/src/ui/commands.py +0 -3131
  107. package/src/ui/displays.py +0 -239
  108. package/src/ui/loader.py +0 -284
  109. package/src/ui/main.py +0 -643
  110. package/src/ui/prompt_utils.py +0 -113
  111. package/src/ui/setting_selector.py +0 -590
  112. package/src/ui/setup_wizard.py +0 -294
  113. package/src/ui/sub_agent_panel.py +0 -234
  114. package/src/ui/tool_confirmation.py +0 -226
  115. package/src/utils/__init__.py +0 -1
  116. package/src/utils/citation_parser.py +0 -199
  117. package/src/utils/editor.py +0 -207
  118. package/src/utils/gitignore_filter.py +0 -149
  119. package/src/utils/logger.py +0 -254
  120. package/src/utils/paths.py +0 -30
  121. package/src/utils/result_parsers.py +0 -108
  122. package/src/utils/safe_commands.py +0 -243
  123. package/src/utils/settings.py +0 -195
  124. package/src/utils/user_message_logger.py +0 -120
  125. package/src/utils/validation.py +0 -201
  126. package/src/utils/web_search.py +0 -173
@@ -1,494 +0,0 @@
1
-
2
-
3
- # Modular prompt composition for native function calling
4
- #
5
- # All prompt sections are loaded from file-based variants under prompts/<variant>/.
6
- # Each variant directory contains individual .md files for each section.
7
- # Section ordering is defined programmatically in _main_sections() and
8
- # _sub_agent_sections() — no manifest files needed.
9
-
10
- import logging
11
-
12
- logger = logging.getLogger(__name__)
13
- from pathlib import Path
14
- from string import Template
15
-
16
- # Root of the prompts directory (repo root / prompts)
17
- _PROMPTS_DIR = Path(__file__).resolve().parents[2] / "prompts"
18
-
19
-
20
- # Mode section for main agent
21
-
22
- MODE_SECTION = """## Current mode: Edit
23
-
24
- **Important:** Explain changes conceptually, show code only in edit tools
25
-
26
- Workflow:
27
- 1. Analyze request and identify files to modify
28
- 2. Generate a brief plan (what/where/why, no code)
29
- 3. **Check for trade-offs** - If multiple valid approaches exist, use select_option to clarify
30
- 4. Proceed with edits
31
-
32
- When the user asks for a plan (e.g. "plan this out", "what's involved", "before you start"):
33
- - Explore and understand requirements first
34
- - Propose a structured plan with bullet points: what changes, where, and why
35
- - Highlight trade-offs and ambiguities using select_option
36
- - End with a summary of the proposed changes
37
- - Ask: 'Do you approve this plan?' before proceeding with edits
38
-
39
- Show code only when using `edit_file`/`create_file` tools. Keep text explanations concise."""
40
-
41
-
42
- # Sub-agent specific sections (research-focused, read-only tools passed via function calling)
43
-
44
- SUB_AGENT_SECTIONS = {
45
- "token_budget": """## Token Budget
46
-
47
- You have a total budget of approximately $hard_limit tokens for this task. When you reach $soft_limit tokens, you MUST immediately stop exploring and return your findings to the main agent. Do not continue reading files, searching, or making tool calls once you are near or past the soft limit. Wrap up your answer with citations and return it promptly.""",
48
-
49
- "response_format": """# Response Format
50
-
51
- When answering the main agent's query:
52
-
53
- 1. **Provide a clear summary** of your findings
54
- 2. **Cite only the most relevant files with precise line ranges** for code you've actually read
55
-
56
- **Important:** Only cite files where you have actually read the content. The main agent will
57
- inject the actual file contents based on your citations and will trust these injected contents
58
- without re-reading them.
59
-
60
- **Required:** You must use bracketed citation formats only. Unbracketed formats like `file:N`
61
- will not be recognized and will be ignored.
62
-
63
- Use these citation formats:
64
- - `[path/to/file] (lines N-M)` - for a specific range you've fully read (preferred)
65
- - `[path/to/file]:N-M` - bracketed range notation (preferred)
66
- - `[path/to/file]:N` - bracketed single line notation (preferred)
67
- - `[path/to/file] (full)` - only for small files or when you genuinely need the entire file
68
-
69
- **Citation Guidelines - Be Selective:**
70
- - Be precise with line numbers - cite only the specific ranges that matter
71
- - Prioritize specific ranges (lines N-M) over full files
72
- - Avoid citing large files with (full) - use specific ranges instead
73
- - Omit boilerplate, tests, and utility code unless directly relevant
74
- - The main agent can always request more context if needed
75
-
76
- Example:
77
- "The authentication flow starts in [src/core/auth.py] (lines 45-78) where tokens are validated,
78
- then calls [src/core/session.py] (lines 112-145) for session management."
79
-
80
- The main agent will automatically inject the actual file contents based on your citations,
81
- so the main agent doesn't need to re-read files you've already explored.""",
82
-
83
- "mode": """# Current mode: Research
84
-
85
- You are a research sub-agent. Answer the specific question asked — do not explore the whole subsystem. Use read-only tools (rg, read_file, list_directory) to gather just enough information.
86
-
87
- **Stop early:** Answer when you can address the query. The main agent can call you again for follow-up if needed. Prefer the most likely paths based on codebase structure.""",
88
-
89
- "review_mode": """# Current mode: Code Review
90
-
91
- You are a code review agent. Analyze the provided git diff and provide honest, useful feedback.
92
- Your output goes directly to the user — write clean, readable markdown.
93
-
94
- ## Workflow
95
- 1. Parse file paths from diff headers (`+++ b/` or `--- a/`)
96
- 2. Use `read_file` on each changed file for surrounding context
97
- 3. Cross-reference related files when needed
98
- 4. Write your review
99
-
100
- ## Output Template
101
-
102
- Follow this exact structure. Do not add extra sections or reorder.
103
-
104
- ### Summary
105
- One paragraph (2-4 sentences). What changed, overall quality. If nothing noteworthy, say so.
106
-
107
- ### Issues
108
- Group issues by severity under sub-headings. Only include levels that have findings.
109
-
110
- #### Critical (N)
111
- - `[path/to/file]:line` — short description
112
-
113
- #### Warning (N)
114
- - `[path/to/file]:line` — short description
115
-
116
- #### Info (N)
117
- - `[path/to/file]:line` — short description
118
-
119
- Severity levels:
120
- - **critical** — Blocking. Must fix before merge. Use sparingly.
121
- - **warning** — Should fix, not blocking.
122
- - **info** — Style, naming, nitpicks.
123
-
124
- One bullet per issue. One line each. No paragraphs. Keep descriptions brief.
125
-
126
- ### Verdict
127
- Always end with a verdict. One line: `APPROVE - explanation` or `REQUEST CHANGES - explanation`.
128
- - `APPROVE` — no critical issues. Mention what looked good or minor nits.
129
- - `REQUEST CHANGES` — critical issues found. Summarize what needs fixing.
130
-
131
- ## Anti-Fabrication Rule
132
- Do not manufacture issues or inflate severity. If nothing is wrong, say so in the summary and skip those labels. An honest "No issues found" beats a fabricated nitpick. Use bracketed citations: `[path/to/file]:line_number`.""",
133
- }
134
-
135
-
136
- # Builder functions to compose prompts from sections
137
-
138
- # Mapping of prompt section keys to the tool names they depend on.
139
- # If ALL listed tools are disabled, the section is omitted from the prompt.
140
- # Sections not listed have no tool dependency and are always included.
141
- SECTION_TOOL_DEPS = {
142
- "trust_subagent_context": ["sub_agent"],
143
- "when_to_use_sub_agent": ["sub_agent"],
144
- "ask_questions": ["select_option"],
145
- "editing_pattern": ["edit_file"],
146
- "task_lists_pattern": ["create_task_list", "complete_task", "show_task_list", "edit_file"],
147
- "temp_folder": ["create_file"],
148
- }
149
-
150
-
151
- def _build_memory_section() -> str | None:
152
- """Build the read-only memory context section for the system prompt.
153
-
154
- Injects live memory content blocks with capacity headers.
155
- No writing instructions — memory files are read-only during conversations.
156
- All writes happen through the dream cron job.
157
-
158
- Returns None if MemoryManager is not initialized or memory is disabled.
159
- """
160
- from llm.config import MEMORY_SETTINGS
161
- if not MEMORY_SETTINGS.get("enabled", True):
162
- return None
163
-
164
- try:
165
- from core.memory import MemoryManager
166
- manager = MemoryManager.get_instance()
167
- if manager is None:
168
- return None
169
-
170
- result = ""
171
-
172
- # Append capacity headers and memory content if files have real content
173
- user_content = manager.load_user_memory()
174
- user_usage = manager.get_user_usage()
175
- if manager._has_entries(user_content):
176
- pct = user_usage["chars_used"] * 100 // user_usage["chars_limit"]
177
- result += f"USER MEMORY [{pct}% — {user_usage['chars_used']}/{user_usage['chars_limit']} chars]\n{user_content.strip()}\n\n"
178
-
179
- project_content = manager.load_project_memory()
180
- project_usage = manager.get_project_usage()
181
- if manager._has_entries(project_content):
182
- pct = project_usage["chars_used"] * 100 // project_usage["chars_limit"]
183
- result += f"PROJECT MEMORY [{pct}% — {project_usage['chars_used']}/{project_usage['chars_limit']} chars]\n{project_content.strip()}\n\n"
184
-
185
- return result.strip() if result else None
186
- except Exception:
187
- return None
188
-
189
-
190
- def _build_vault_section(variant: str = "main") -> str | None:
191
- """Build the Obsidian vault section for the system prompt.
192
-
193
- Loads obsidian.md from prompts/<variant>/ and substitutes dynamic values
194
- (vault root, project folder, excluded folders) using string.Template.
195
- If project exists, also loads and appends obsidian_project.md from the
196
- same variant directory.
197
-
198
- Returns None if vault is not active.
199
- """
200
- try:
201
- from utils.settings import obsidian_settings
202
- if not obsidian_settings.is_active():
203
- return None
204
- except Exception as e:
205
- logger.debug("Obsidian not available: %s", e)
206
- return None
207
-
208
- try:
209
- from tools.obsidian import get_vault_session, init_session
210
- session = get_vault_session()
211
- # Initialize session on first prompt build if not yet available.
212
- # Normally initialized by AgenticLoop.__init__, but the system prompt
213
- # is built earlier (in ChatManager.__init__), causing an inconsistent
214
- # vault section (missing note schemas) on fresh start.
215
- if session is None:
216
- session = init_session()
217
- except Exception:
218
- session = None
219
-
220
- vault_root = str(session.vault_root) if session else "<not available>"
221
- project_folder = str(session.project_folder) if session else "<not available>"
222
-
223
- project_exists = (
224
- session
225
- and session.project_folder.is_dir()
226
- and (session.project_folder / "Bugs").is_dir()
227
- )
228
-
229
- excluded = obsidian_settings.exclude_folders
230
-
231
- # Load base obsidian template from variant directory
232
- base_path = _PROMPTS_DIR / variant / "obsidian.md"
233
- if not base_path.is_file():
234
- logger.warning(
235
- "Obsidian template not found at %s — vault section omitted", base_path
236
- )
237
- return None
238
-
239
- base_content = base_path.read_text(encoding="utf-8").strip()
240
-
241
- # Substitute dynamic values using string.Template
242
- if project_exists:
243
- project_header = f"**Project folder:** `{project_folder}`"
244
- else:
245
- project_header = "**Project:** not initialized (run `/obsidian init` to create)"
246
-
247
- formatted = Template(base_content).safe_substitute(
248
- vault_root=vault_root,
249
- project_folder=project_folder,
250
- project_header=project_header,
251
- excluded=excluded,
252
- )
253
-
254
- # Append project-specific section if project exists
255
- if project_exists:
256
- project_path = _PROMPTS_DIR / variant / "obsidian_project.md"
257
- if project_path.is_file():
258
- project_content = project_path.read_text(encoding="utf-8").strip()
259
- formatted = formatted + "\n\n" + project_content
260
-
261
- return formatted
262
-
263
-
264
- def _build_context_section() -> str:
265
- """Build a dynamic section with current date and location."""
266
- from datetime import datetime
267
- import os
268
-
269
- now = datetime.now()
270
- date_str = now.strftime("%A, %B %d, %Y")
271
-
272
- return (
273
- "## Current Context\n\n"
274
- f"**Date:** {date_str}\n"
275
- f"**Working directory:** {os.getcwd()}\n"
276
- )
277
-
278
-
279
- def _static(variant: str, name: str) -> str:
280
- """Load a static .md section from prompts/<variant>/."""
281
- path = _PROMPTS_DIR / variant / name
282
- if path.is_file():
283
- return path.read_text(encoding="utf-8").strip()
284
- logger.warning("Section file not found: %s — prompt section '%s' omitted", path, name)
285
- return ""
286
-
287
-
288
- def _resolve_variant() -> str:
289
- """Resolve the prompt variant from settings, falling back to 'main'."""
290
- try:
291
- from utils.settings import prompt_settings
292
- return prompt_settings.variant
293
- except Exception:
294
- return "main"
295
-
296
-
297
- def _should_include_section(section_key: str) -> bool:
298
- """Check whether a prompt section should be included based on tool availability.
299
-
300
- A section is skipped only when ALL of its dependent tools are disabled.
301
- Uses lazy import to avoid circular dependency with tools module.
302
- """
303
- deps = SECTION_TOOL_DEPS.get(section_key)
304
- if not deps:
305
- return True
306
- from tools.helpers.base import ToolRegistry
307
- return not all(ToolRegistry.is_disabled(t) for t in deps)
308
-
309
-
310
- def _build_prompt_to_list(sections: list[tuple[str, callable]]) -> list[str]:
311
- """Build prompt as a list of section strings from (key, content_fn) pairs.
312
-
313
- Skips sections that fail _should_include_section (tool deps)
314
- or whose content_fn returns None/empty.
315
- """
316
- result = []
317
- for key, content_fn in sections:
318
- if not _should_include_section(key):
319
- continue
320
- content = content_fn()
321
- if content:
322
- result.append(content)
323
- return result
324
-
325
-
326
- def _build_prompt(sections: list[tuple[str, callable]]) -> str:
327
- """Build prompt string from (key, content_fn) pairs.
328
-
329
- Delegates to _build_prompt_to_list and joins the result.
330
- """
331
- return "\n\n".join(_build_prompt_to_list(sections))
332
-
333
-
334
- def _variant_available(variant: str) -> bool:
335
- """Check if a variant directory exists.
336
-
337
- Raises OSError if directory permissions prevent access.
338
- """
339
- return (_PROMPTS_DIR / variant).is_dir()
340
-
341
-
342
- def _list_variants() -> list[str]:
343
- """List available prompt variants (directories under prompts/)."""
344
- if not _PROMPTS_DIR.is_dir():
345
- return []
346
- return sorted(
347
- d.name for d in _PROMPTS_DIR.iterdir()
348
- if d.is_dir()
349
- )
350
-
351
-
352
- def _main_sections(variant: str) -> list[tuple[str, callable]]:
353
- """Return (key, content_fn) pairs for the main agent prompt.
354
-
355
- Order in this list = order in the final prompt.
356
- Static .md sections, dynamic builders, and hardcoded sections all
357
- live here — single source of truth for ordering.
358
- """
359
- return [
360
- ("intro", lambda: _static(variant, "intro.md")),
361
- ("context", _build_context_section),
362
- ("tone_and_style", lambda: _static(variant, "tone_and_style.md")),
363
- ("communication_style", lambda: _static(variant, "communication_style.md")),
364
- ("trust_subagent_context", lambda: _static(variant, "trust_subagent_context.md")),
365
- ("context_reliability", lambda: _static(variant, "context_reliability.md")),
366
- ("skills", lambda: _static(variant, "skills.md")),
367
- ("conversational_tool_calling", lambda: _static(variant, "conversational_tool_calling.md")),
368
- ("professional_objectivity", lambda: _static(variant, "professional_objectivity.md")),
369
- ("think_before_acting", lambda: _static(variant, "think_before_acting.md")),
370
- ("batch_independent_calls", lambda: _static(variant, "batch_independent_calls.md")),
371
- ("code_references", lambda: _static(variant, "code_references.md")),
372
- ("exploration_pattern", lambda: _static(variant, "exploration_pattern.md")),
373
- ("targeted_searching", lambda: _static(variant, "targeted_searching.md")),
374
- ("editing_pattern", lambda: _static(variant, "editing_pattern.md")),
375
- ("task_lists_pattern", lambda: _static(variant, "task_lists_pattern.md")),
376
- ("casual_interactions", lambda: _static(variant, "casual_interactions.md")),
377
- ("ask_questions", lambda: _static(variant, "ask_questions.md")),
378
- ("tool_preferences", lambda: _static(variant, "tool_preferences.md")),
379
- ("when_to_use_sub_agent", lambda: _static(variant, "when_to_use_sub_agent.md")),
380
- ("error_handling", lambda: _static(variant, "error_handling.md")),
381
- ("temp_folder", lambda: _static(variant, "temp_folder.md")),
382
- ("memory_system", _build_memory_section),
383
- ("obsidian", lambda: _build_vault_section(variant)),
384
- ("mode", lambda: MODE_SECTION),
385
- ]
386
-
387
-
388
- def _sub_agent_sections(variant: str) -> list[tuple[str, callable]]:
389
- """Return (key, content_fn) pairs for the sub-agent prompt.
390
-
391
- The micro variant has a smaller set of sections than main.
392
- response_format is placed explicitly after code_references.
393
- """
394
- # Base sections shared across all variants
395
- base = [
396
- ("intro", lambda: _static(variant, "intro.md")),
397
- ("context", _build_context_section),
398
- ("tone_and_style", lambda: _static(variant, "tone_and_style.md")),
399
- ("communication_style", lambda: _static(variant, "communication_style.md")),
400
- ]
401
-
402
- # Micro variant has a different section set (no conversational_tool_calling,
403
- # no professional_objectivity, no think_before_acting, etc.)
404
- if variant == "micro":
405
- middle = [
406
- ("trust_subagent_context", lambda: _static(variant, "trust_subagent_context.md")),
407
- ("context_reliability", lambda: _static(variant, "context_reliability.md")),
408
- ("skills", lambda: _static(variant, "skills.md")),
409
- ("exploration_pattern", lambda: _static(variant, "exploration_pattern.md")),
410
- ("targeted_searching", lambda: _static(variant, "targeted_searching.md")),
411
- ("tool_preferences", lambda: _static(variant, "tool_preferences.md")),
412
- ]
413
- else:
414
- middle = [
415
- ("skills", lambda: _static(variant, "skills.md")),
416
- ("conversational_tool_calling", lambda: _static(variant, "conversational_tool_calling.md")),
417
- ("professional_objectivity", lambda: _static(variant, "professional_objectivity.md")),
418
- ("think_before_acting", lambda: _static(variant, "think_before_acting.md")),
419
- ("batch_independent_calls", lambda: _static(variant, "batch_independent_calls.md")),
420
- ("code_references", lambda: _static(variant, "code_references.md")),
421
- ("response_format", lambda: SUB_AGENT_SECTIONS["response_format"]),
422
- ("exploration_pattern", lambda: _static(variant, "exploration_pattern.md")),
423
- ("targeted_searching", lambda: _static(variant, "targeted_searching.md")),
424
- ("casual_interactions", lambda: _static(variant, "casual_interactions.md")),
425
- ("temp_folder", lambda: _static(variant, "temp_folder.md")),
426
- ]
427
-
428
- return base + middle
429
-
430
-
431
- def build_system_prompt(variant: str | None = None, active_skills_section: str = "") -> str:
432
- """Build system prompt for main agent.
433
-
434
- Loads section content from prompts/<variant>/. Order is defined by
435
- _main_sections(). Raises FileNotFoundError if variant directory is missing.
436
-
437
- Args:
438
- variant: Variant name (e.g. 'main', 'micro').
439
- If None, reads from settings.
440
- active_skills_section: Optional rendered active-skills block to append.
441
-
442
- Returns:
443
- Complete system prompt string
444
- """
445
- if variant is None:
446
- variant = _resolve_variant()
447
- if not _variant_available(variant):
448
- raise FileNotFoundError(
449
- f"Prompt variant '{variant}' not found: "
450
- f"{_PROMPTS_DIR / variant} does not exist"
451
- )
452
- result = _build_prompt(_main_sections(variant))
453
- if active_skills_section.strip():
454
- result += "\n\n" + active_skills_section.strip()
455
- return result
456
-
457
-
458
- def build_sub_agent_prompt(sub_agent_type: str = "research", soft_limit_tokens: int | None = None, hard_limit_tokens: int | None = None) -> str:
459
- """Build prompt for sub-agent (research or review, read-only).
460
-
461
- Args:
462
- sub_agent_type: Type of sub-agent ('research' or 'review').
463
- soft_limit_tokens: Soft token limit to display in prompt.
464
- hard_limit_tokens: Hard token limit to display in prompt.
465
-
466
- Returns:
467
- Complete system prompt string
468
- """
469
- variant = _resolve_variant()
470
- if not _variant_available(variant):
471
- raise FileNotFoundError(
472
- f"Sub-agent prompt variant '{variant}' not found: "
473
- f"{_PROMPTS_DIR / variant} does not exist"
474
- )
475
-
476
- result = _build_prompt_to_list(_sub_agent_sections(variant))
477
-
478
- # Append parameterized sections (always last)
479
- if soft_limit_tokens is not None and hard_limit_tokens is not None:
480
- result.append(
481
- Template(SUB_AGENT_SECTIONS["token_budget"]).safe_substitute(
482
- soft_limit=f"{soft_limit_tokens:,}",
483
- hard_limit=f"{hard_limit_tokens:,}",
484
- )
485
- )
486
-
487
- if sub_agent_type == "review":
488
- result.append(SUB_AGENT_SECTIONS["review_mode"])
489
- else:
490
- result.append(SUB_AGENT_SECTIONS["mode"])
491
-
492
- return "\n\n".join(result)
493
-
494
-