aru-code 0.36.0__tar.gz → 0.37.0__tar.gz
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.
- {aru_code-0.36.0/aru_code.egg-info → aru_code-0.37.0}/PKG-INFO +1 -1
- aru_code-0.37.0/aru/__init__.py +1 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/aru/config.py +14 -1
- {aru_code-0.36.0 → aru_code-0.37.0}/aru/memory/__init__.py +9 -1
- {aru_code-0.36.0 → aru_code-0.37.0}/aru/memory/store.py +40 -9
- {aru_code-0.36.0 → aru_code-0.37.0}/aru/runner.py +20 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/aru/tools/memory_tool.py +63 -5
- {aru_code-0.36.0 → aru_code-0.37.0}/aru/tools/registry.py +2 -2
- {aru_code-0.36.0 → aru_code-0.37.0/aru_code.egg-info}/PKG-INFO +1 -1
- {aru_code-0.36.0 → aru_code-0.37.0}/aru_code.egg-info/SOURCES.txt +1 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/pyproject.toml +1 -1
- {aru_code-0.36.0 → aru_code-0.37.0}/tests/test_memory.py +82 -1
- aru_code-0.37.0/tests/test_runner_interrupt.py +70 -0
- aru_code-0.36.0/aru/__init__.py +0 -1
- {aru_code-0.36.0 → aru_code-0.37.0}/LICENSE +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/README.md +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/aru/agent_factory.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/aru/agents/__init__.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/aru/agents/base.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/aru/agents/catalog.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/aru/agents/planner.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/aru/cache_patch.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/aru/checkpoints.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/aru/cli.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/aru/commands.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/aru/completers.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/aru/context.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/aru/display.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/aru/events.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/aru/format/__init__.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/aru/format/manager.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/aru/format/runner.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/aru/history_blocks.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/aru/lsp/__init__.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/aru/lsp/client.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/aru/lsp/manager.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/aru/lsp/protocol.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/aru/memory/extractor.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/aru/memory/loader.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/aru/permissions.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/aru/plugin_cache.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/aru/plugins/__init__.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/aru/plugins/custom_tools.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/aru/plugins/hooks.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/aru/plugins/manager.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/aru/plugins/tool_api.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/aru/providers.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/aru/runtime.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/aru/select.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/aru/session.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/aru/sinks.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/aru/streaming.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/aru/tool_policy.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/aru/tools/__init__.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/aru/tools/_diff.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/aru/tools/_shared.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/aru/tools/apply_patch.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/aru/tools/apply_patch_prompt.txt +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/aru/tools/ast_tools.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/aru/tools/codebase.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/aru/tools/delegate.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/aru/tools/delegate_prompt.txt +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/aru/tools/file_ops.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/aru/tools/gitignore.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/aru/tools/lsp.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/aru/tools/mcp_client.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/aru/tools/plan_mode.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/aru/tools/ranker.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/aru/tools/search.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/aru/tools/shell.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/aru/tools/skill.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/aru/tools/tasklist.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/aru/tools/web.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/aru/tools/worktree.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/aru/tui/__init__.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/aru/tui/app.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/aru/tui/screens/__init__.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/aru/tui/screens/choice.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/aru/tui/screens/confirm.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/aru/tui/screens/search.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/aru/tui/screens/text_input.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/aru/tui/sinks.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/aru/tui/slash_bridge.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/aru/tui/ui.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/aru/tui/widgets/__init__.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/aru/tui/widgets/chat.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/aru/tui/widgets/completer.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/aru/tui/widgets/context_pane.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/aru/tui/widgets/header.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/aru/tui/widgets/inline_choice.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/aru/tui/widgets/loaded_pane.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/aru/tui/widgets/status.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/aru/tui/widgets/thinking.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/aru/tui/widgets/tools.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/aru/ui.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/aru_code.egg-info/dependency_links.txt +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/aru_code.egg-info/entry_points.txt +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/aru_code.egg-info/requires.txt +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/aru_code.egg-info/top_level.txt +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/setup.cfg +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/tests/test_agents_base.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/tests/test_agents_md_coverage.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/tests/test_apply_patch.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/tests/test_async_tool_permission.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/tests/test_cache_patch_metrics.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/tests/test_cache_patch_stop_reason.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/tests/test_catalog.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/tests/test_chat_scrollable.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/tests/test_checkpoints.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/tests/test_cli.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/tests/test_cli_advanced.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/tests/test_cli_base.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/tests/test_cli_completers.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/tests/test_cli_new.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/tests/test_cli_run_cli.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/tests/test_cli_session.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/tests/test_cli_shell.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/tests/test_codebase.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/tests/test_confabulation_regression.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/tests/test_config.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/tests/test_context.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/tests/test_context_pane.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/tests/test_cwd_awareness.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/tests/test_delegate.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/tests/test_events_backward_compat.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/tests/test_events_schema.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/tests/test_fork_ctx_concurrency.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/tests/test_format.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/tests/test_gitignore.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/tests/test_guardrails_scenarios.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/tests/test_invoke_skill.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/tests/test_invoked_skills.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/tests/test_loaded_pane_path.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/tests/test_lsp.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/tests/test_lsp_rename.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/tests/test_main.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/tests/test_markdown_to_text.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/tests/test_mcp_client.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/tests/test_mcp_health.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/tests/test_memory_tool.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/tests/test_microcompact.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/tests/test_permissions.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/tests/test_plan_mode_refactor.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/tests/test_plugin_cache.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/tests/test_plugin_errors.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/tests/test_plugin_hooks_v2.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/tests/test_plugins.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/tests/test_providers.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/tests/test_ranker.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/tests/test_reasoning.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/tests/test_runner_recovery.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/tests/test_runtime.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/tests/test_select.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/tests/test_skill_disallowed_tools.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/tests/test_status_breakdown.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/tests/test_status_cost.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/tests/test_streaming_sink.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/tests/test_tasklist.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/tests/test_thread_tool_timeout.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/tests/test_tool_policy.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/tests/test_truncation_marker.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/tests/test_tui_app_boot.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/tests/test_tui_bindings.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/tests/test_tui_bus_flow.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/tests/test_tui_chat.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/tests/test_tui_completer.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/tests/test_tui_completer_dynamic.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/tests/test_tui_copy.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/tests/test_tui_input_behaviour.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/tests/test_tui_mention_expand.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/tests/test_tui_modals.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/tests/test_tui_mode_cycle.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/tests/test_tui_native_selection.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/tests/test_tui_permission_flow.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/tests/test_tui_plan_task_render.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/tests/test_tui_sidebar_toggle.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/tests/test_tui_slash_bridge.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/tests/test_tui_snapshot_smoke.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/tests/test_tui_thinking_and_boot.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/tests/test_tui_widgets_visual.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/tests/test_ui_adapter.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/tests/test_worktree.py +0 -0
- {aru_code-0.36.0 → aru_code-0.37.0}/tests/test_worktree_session_restore.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.37.0"
|
|
@@ -227,7 +227,20 @@ class AgentConfig:
|
|
|
227
227
|
try:
|
|
228
228
|
from aru.memory.loader import memory_section_for_prompt
|
|
229
229
|
import os
|
|
230
|
-
|
|
230
|
+
# Prefer session.project_root (stable across subdir/worktree
|
|
231
|
+
# invocations) so memory is keyed to the project, not to
|
|
232
|
+
# whichever cwd `aru` happened to be launched from. Fall
|
|
233
|
+
# back to os.getcwd() during bootstrap or outside a ctx.
|
|
234
|
+
project_root = os.getcwd()
|
|
235
|
+
try:
|
|
236
|
+
from aru.runtime import get_ctx
|
|
237
|
+
_session = getattr(get_ctx(), "session", None)
|
|
238
|
+
_pr = getattr(_session, "project_root", None) if _session else None
|
|
239
|
+
if _pr:
|
|
240
|
+
project_root = _pr
|
|
241
|
+
except LookupError:
|
|
242
|
+
pass
|
|
243
|
+
section = memory_section_for_prompt(project_root)
|
|
231
244
|
if section:
|
|
232
245
|
parts.append(section.strip())
|
|
233
246
|
except Exception: # pragma: no cover — memory module failure mustn't break prompts
|
|
@@ -10,11 +10,19 @@ Components:
|
|
|
10
10
|
|
|
11
11
|
Storage layout:
|
|
12
12
|
|
|
13
|
-
~/.aru/projects/<
|
|
13
|
+
~/.aru/projects/<path-encoded>/memory/
|
|
14
14
|
├── MEMORY.md # one-line-per-memory index
|
|
15
15
|
├── feedback_*.md # one file per memory, YAML frontmatter + body
|
|
16
16
|
└── user_*.md
|
|
17
17
|
|
|
18
|
+
``<path-encoded>`` mirrors Claude Code's scheme: every non-alphanumeric
|
|
19
|
+
character in ``abspath(project_root)`` becomes a dash. Example::
|
|
20
|
+
|
|
21
|
+
D:\\OneDrive\\python_projects\\aru -> D--OneDrive-python-projects-aru
|
|
22
|
+
|
|
23
|
+
The directory is created lazily on the first ``write_memory`` call, so a
|
|
24
|
+
project that never writes a memory never leaves an empty folder behind.
|
|
25
|
+
|
|
18
26
|
Config (aru.json):
|
|
19
27
|
|
|
20
28
|
{
|
|
@@ -11,7 +11,6 @@ etc. until free.
|
|
|
11
11
|
|
|
12
12
|
from __future__ import annotations
|
|
13
13
|
|
|
14
|
-
import hashlib
|
|
15
14
|
import os
|
|
16
15
|
import re
|
|
17
16
|
from dataclasses import dataclass
|
|
@@ -44,18 +43,48 @@ class MemoryEntry:
|
|
|
44
43
|
|
|
45
44
|
# ── Paths ────────────────────────────────────────────────────────────
|
|
46
45
|
|
|
47
|
-
|
|
48
|
-
return hashlib.sha256(os.path.abspath(project_root).encode("utf-8")).hexdigest()[:12]
|
|
46
|
+
_PATH_ENCODE_RE = re.compile(r"[^A-Za-z0-9]")
|
|
49
47
|
|
|
50
48
|
|
|
51
|
-
def
|
|
52
|
-
"""
|
|
49
|
+
def _encode_project_path(project_root: str) -> str:
|
|
50
|
+
"""Encode an absolute project path as a human-readable folder name.
|
|
53
51
|
|
|
54
|
-
|
|
52
|
+
Matches Claude Code's scheme: every non-alphanumeric character (drive
|
|
53
|
+
colon, path separator, underscore, dot, dash, space, ...) becomes a
|
|
54
|
+
single dash, without collapsing runs. So ``D:\\`` → ``D--`` (colon
|
|
55
|
+
AND backslash each become a dash).
|
|
56
|
+
|
|
57
|
+
A quick ``ls ~/.aru/projects`` then shows which folder belongs to
|
|
58
|
+
which project without needing to decode a hash.
|
|
59
|
+
|
|
60
|
+
Examples::
|
|
61
|
+
|
|
62
|
+
D:\\OneDrive\\python_projects\\aru -> D--OneDrive-python-projects-aru
|
|
63
|
+
/home/u/proj -> -home-u-proj
|
|
64
|
+
"""
|
|
65
|
+
return _PATH_ENCODE_RE.sub("-", os.path.abspath(project_root))
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def memory_dir_for_project(
|
|
69
|
+
project_root: str,
|
|
70
|
+
base: str | None = None,
|
|
71
|
+
*,
|
|
72
|
+
create: bool = False,
|
|
73
|
+
) -> Path:
|
|
74
|
+
"""Return the memory directory path for *project_root*.
|
|
75
|
+
|
|
76
|
+
``~/.aru/projects/<path-encoded>/memory`` by default. Override ``base``
|
|
77
|
+
for tests.
|
|
78
|
+
|
|
79
|
+
The directory is **not** created on disk unless ``create=True``. Read
|
|
80
|
+
paths (``load_memory_index``, ``list_memories``) pass the default so
|
|
81
|
+
a project that never writes a memory leaves no empty folder behind.
|
|
82
|
+
``write_memory`` passes ``create=True``.
|
|
55
83
|
"""
|
|
56
84
|
base_path = Path(base) if base else Path.home() / ".aru" / "projects"
|
|
57
|
-
d = base_path /
|
|
58
|
-
|
|
85
|
+
d = base_path / _encode_project_path(project_root) / "memory"
|
|
86
|
+
if create:
|
|
87
|
+
d.mkdir(parents=True, exist_ok=True)
|
|
59
88
|
return d
|
|
60
89
|
|
|
61
90
|
|
|
@@ -153,7 +182,7 @@ def write_memory(project_root: str, entry: MemoryEntry,
|
|
|
153
182
|
f"Invalid memory type {entry.type!r}; must be one of "
|
|
154
183
|
f"{sorted(VALID_MEMORY_TYPES)}."
|
|
155
184
|
)
|
|
156
|
-
mem_dir = memory_dir_for_project(project_root, base=base)
|
|
185
|
+
mem_dir = memory_dir_for_project(project_root, base=base, create=True)
|
|
157
186
|
slug = _unique_slug(mem_dir, _slugify(entry.name, entry.type))
|
|
158
187
|
entry.slug = slug
|
|
159
188
|
(mem_dir / entry.filename).write_text(_render_memory_file(entry), encoding="utf-8")
|
|
@@ -239,6 +268,8 @@ def search_memories(
|
|
|
239
268
|
def delete_memory(project_root: str, slug: str, base: str | None = None) -> bool:
|
|
240
269
|
"""Delete the memory file + remove its index line. True if something was removed."""
|
|
241
270
|
mem_dir = memory_dir_for_project(project_root, base=base)
|
|
271
|
+
if not mem_dir.exists():
|
|
272
|
+
return False
|
|
242
273
|
path = mem_dir / f"{slug}.md"
|
|
243
274
|
removed = path.exists()
|
|
244
275
|
if removed:
|
|
@@ -670,6 +670,26 @@ async def run_agent_capture(agent, message: str, session=None, lightweight: bool
|
|
|
670
670
|
except Exception:
|
|
671
671
|
pass
|
|
672
672
|
console.print("\n[yellow]Interrupted.[/yellow]")
|
|
673
|
+
# Python 3.11+: ``asyncio.run`` cancels the main task on SIGINT.
|
|
674
|
+
# Catching CancelledError here is not enough — the task stays in a
|
|
675
|
+
# "cancelling" state, so the caller's NEXT await (typically the
|
|
676
|
+
# REPL prompt in cli.py) re-raises CancelledError immediately,
|
|
677
|
+
# which looks to the user like "Ctrl+C exits aru". ``uncancel()``
|
|
678
|
+
# resets that counter so the turn ends cleanly and the REPL keeps
|
|
679
|
+
# running.
|
|
680
|
+
try:
|
|
681
|
+
current = asyncio.current_task()
|
|
682
|
+
if current is not None:
|
|
683
|
+
current.uncancel()
|
|
684
|
+
except Exception:
|
|
685
|
+
pass
|
|
686
|
+
# Mirror the TUI's reset at turn start — clear the shared abort
|
|
687
|
+
# flag so the next turn isn't short-circuited by leftover state.
|
|
688
|
+
try:
|
|
689
|
+
from aru.runtime import reset_abort
|
|
690
|
+
reset_abort()
|
|
691
|
+
except Exception:
|
|
692
|
+
pass
|
|
673
693
|
except Exception as e:
|
|
674
694
|
try:
|
|
675
695
|
sink.exit()
|
|
@@ -1,17 +1,20 @@
|
|
|
1
|
-
"""Agent-facing memory
|
|
1
|
+
"""Agent-facing memory tools — Tier 3 #3.
|
|
2
2
|
|
|
3
|
-
Exposes the per-project memory store
|
|
4
|
-
as a read-only tool. Two modes:
|
|
3
|
+
Exposes the per-project memory store as a pair of tools:
|
|
5
4
|
|
|
6
5
|
- ``memory_search(slug="...")`` → returns the full body of one memory
|
|
7
6
|
- ``memory_search(query="...")`` → keyword substring search over
|
|
8
7
|
name / description / body, returns a
|
|
9
8
|
ranked list with 200-char previews
|
|
10
9
|
- ``memory_search()`` → summary stats by type
|
|
10
|
+
- ``memory_write(name, body, ...)`` → persist a new memory explicitly,
|
|
11
|
+
without waiting for the turn.end
|
|
12
|
+
extractor
|
|
11
13
|
|
|
12
14
|
The system prompt already receives ``MEMORY.md`` as an index at startup
|
|
13
|
-
(Tier 2 #4).
|
|
14
|
-
|
|
15
|
+
(Tier 2 #4). ``memory_search`` is the read complement; ``memory_write``
|
|
16
|
+
lets the agent honour direct user requests like "save X to memory" that
|
|
17
|
+
would otherwise fall through the extractor's threshold.
|
|
15
18
|
"""
|
|
16
19
|
|
|
17
20
|
from __future__ import annotations
|
|
@@ -20,10 +23,12 @@ import os
|
|
|
20
23
|
from collections import Counter
|
|
21
24
|
|
|
22
25
|
from aru.memory.store import (
|
|
26
|
+
MemoryEntry,
|
|
23
27
|
VALID_MEMORY_TYPES,
|
|
24
28
|
list_memories,
|
|
25
29
|
read_memory,
|
|
26
30
|
search_memories,
|
|
31
|
+
write_memory,
|
|
27
32
|
)
|
|
28
33
|
from aru.runtime import get_ctx
|
|
29
34
|
|
|
@@ -106,3 +111,56 @@ def memory_search(query: str = "", slug: str = "") -> str:
|
|
|
106
111
|
f"\n body: {preview}"
|
|
107
112
|
)
|
|
108
113
|
return "\n".join(lines)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def memory_write(name: str, body: str, type: str = "user",
|
|
117
|
+
description: str = "") -> str:
|
|
118
|
+
"""Persist a durable memory for this project across future sessions.
|
|
119
|
+
|
|
120
|
+
Use when the user explicitly asks to save / remember / "lembra" / "salva"
|
|
121
|
+
something that should survive session boundaries. Pick the type carefully:
|
|
122
|
+
|
|
123
|
+
- ``user`` — user's persistent preferences or workflow rules
|
|
124
|
+
("prefer pytest", "always type hints")
|
|
125
|
+
- ``feedback`` — corrections the user gave ("don't mock the DB, got burned")
|
|
126
|
+
- ``project`` — project-level state / decisions / deadlines / incidents
|
|
127
|
+
- ``reference`` — pointers to external systems (dashboards, tickets, docs)
|
|
128
|
+
|
|
129
|
+
Do NOT save:
|
|
130
|
+
- Code patterns or anything derivable from reading the repo
|
|
131
|
+
- Ephemeral conversation state
|
|
132
|
+
- Duplicates of what is already in the memory index
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
name: Short title (under 60 chars). Used as the memory's display name
|
|
136
|
+
and as the base for its filename slug.
|
|
137
|
+
body: The fact to remember (under 400 chars). Prefer a single
|
|
138
|
+
declarative sentence; future sessions see this verbatim.
|
|
139
|
+
type: One of ``user`` / ``feedback`` / ``project`` / ``reference``.
|
|
140
|
+
Defaults to ``user``.
|
|
141
|
+
description: Optional one-line summary for the MEMORY.md index
|
|
142
|
+
(under 100 chars). If empty, defaults to ``name``.
|
|
143
|
+
"""
|
|
144
|
+
mtype = (type or "user").strip().lower()
|
|
145
|
+
if mtype not in VALID_MEMORY_TYPES:
|
|
146
|
+
return (
|
|
147
|
+
f"Invalid memory type {mtype!r}. Must be one of "
|
|
148
|
+
f"{sorted(VALID_MEMORY_TYPES)}."
|
|
149
|
+
)
|
|
150
|
+
name_clean = (name or "").strip()[:60]
|
|
151
|
+
body_clean = (body or "").strip()[:400]
|
|
152
|
+
desc_clean = (description or "").strip()[:100] or name_clean
|
|
153
|
+
if not name_clean or not body_clean:
|
|
154
|
+
return "memory_write requires both `name` and `body`."
|
|
155
|
+
|
|
156
|
+
entry = MemoryEntry(
|
|
157
|
+
name=name_clean,
|
|
158
|
+
description=desc_clean,
|
|
159
|
+
type=mtype,
|
|
160
|
+
body=body_clean,
|
|
161
|
+
)
|
|
162
|
+
persisted = write_memory(_project_root(), entry)
|
|
163
|
+
return (
|
|
164
|
+
f"Saved memory '{persisted.slug}' ({persisted.type}): "
|
|
165
|
+
f"{persisted.name}"
|
|
166
|
+
)
|
|
@@ -35,7 +35,7 @@ from aru.tools.lsp import (
|
|
|
35
35
|
lsp_references,
|
|
36
36
|
lsp_rename,
|
|
37
37
|
)
|
|
38
|
-
from aru.tools.memory_tool import memory_search
|
|
38
|
+
from aru.tools.memory_tool import memory_search, memory_write
|
|
39
39
|
from aru.tools.web import web_fetch, web_search
|
|
40
40
|
from aru.tools.worktree import worktree_info
|
|
41
41
|
|
|
@@ -82,7 +82,7 @@ _TASK_MGMT_TOOLS = [
|
|
|
82
82
|
# clarity; excluded from subagent / planner / explorer sets.
|
|
83
83
|
_SKILL_TOOLS = [invoke_skill]
|
|
84
84
|
|
|
85
|
-
CORE_TOOLS = _READ_ONLY_TOOLS + _WRITE_TOOLS + [bash] + _NET_TOOLS + [delegate_task]
|
|
85
|
+
CORE_TOOLS = _READ_ONLY_TOOLS + _WRITE_TOOLS + [bash] + _NET_TOOLS + [delegate_task, memory_write]
|
|
86
86
|
|
|
87
87
|
ALL_TOOLS = _TASK_MGMT_TOOLS + _SKILL_TOOLS + CORE_TOOLS
|
|
88
88
|
|
|
@@ -26,6 +26,7 @@ from aru.memory.loader import (
|
|
|
26
26
|
from aru.memory.store import (
|
|
27
27
|
MAX_MEMORIES_PER_PROJECT,
|
|
28
28
|
MemoryEntry,
|
|
29
|
+
_encode_project_path,
|
|
29
30
|
clear_memory,
|
|
30
31
|
delete_memory,
|
|
31
32
|
list_memories,
|
|
@@ -150,7 +151,7 @@ def test_memory_section_includes_body_when_index_exists(project_root, memory_bas
|
|
|
150
151
|
|
|
151
152
|
|
|
152
153
|
def test_load_memory_index_truncates_above_cap(project_root, memory_base):
|
|
153
|
-
mem_dir = memory_dir_for_project(project_root, base=memory_base)
|
|
154
|
+
mem_dir = memory_dir_for_project(project_root, base=memory_base, create=True)
|
|
154
155
|
# Seed a gigantic index file manually
|
|
155
156
|
(mem_dir / "MEMORY.md").write_text(
|
|
156
157
|
"# Memory Index\n\n" + "\n".join(f"- line {i}" for i in range(MAX_INDEX_LINES + 30)),
|
|
@@ -161,6 +162,42 @@ def test_load_memory_index_truncates_above_cap(project_root, memory_base):
|
|
|
161
162
|
assert "truncated" in text
|
|
162
163
|
|
|
163
164
|
|
|
165
|
+
def test_memory_dir_is_lazy_by_default(project_root, memory_base):
|
|
166
|
+
"""Default call must NOT create the dir — avoids phantom empty folders."""
|
|
167
|
+
mem_dir = memory_dir_for_project(project_root, base=memory_base)
|
|
168
|
+
assert not mem_dir.exists()
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def test_memory_dir_creates_when_requested(project_root, memory_base):
|
|
172
|
+
mem_dir = memory_dir_for_project(project_root, base=memory_base, create=True)
|
|
173
|
+
assert mem_dir.exists()
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def test_load_memory_index_does_not_create_dir(project_root, memory_base):
|
|
177
|
+
"""Calling the loader on a clean project must leave the FS untouched."""
|
|
178
|
+
assert load_memory_index(project_root, base=memory_base) == ""
|
|
179
|
+
mem_dir = memory_dir_for_project(project_root, base=memory_base)
|
|
180
|
+
assert not mem_dir.exists()
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def test_encode_project_path_matches_cc_scheme():
|
|
184
|
+
# Windows-style path: drive colon, backslashes, AND underscores all
|
|
185
|
+
# become dashes (matches Claude Code's scheme exactly).
|
|
186
|
+
assert _encode_project_path(
|
|
187
|
+
"D:\\OneDrive\\Documentos\\python_projects\\aru"
|
|
188
|
+
).endswith("D--OneDrive-Documentos-python-projects-aru")
|
|
189
|
+
# Case is preserved; runs of separators are NOT collapsed.
|
|
190
|
+
encoded = _encode_project_path("D:\\Foo_Bar\\baz")
|
|
191
|
+
assert encoded.endswith("D--Foo-Bar-baz")
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def test_delete_memory_missing_dir_returns_false(project_root, memory_base):
|
|
195
|
+
"""Deleting from a project that never wrote any memory is a no-op."""
|
|
196
|
+
assert delete_memory(project_root, "user_whatever", base=memory_base) is False
|
|
197
|
+
mem_dir = memory_dir_for_project(project_root, base=memory_base)
|
|
198
|
+
assert not mem_dir.exists()
|
|
199
|
+
|
|
200
|
+
|
|
164
201
|
# ── Extractor ────────────────────────────────────────────────────────
|
|
165
202
|
|
|
166
203
|
def test_should_trigger_respects_auto_extract_flag():
|
|
@@ -213,3 +250,47 @@ def test_candidate_to_entry_builds_valid_entry():
|
|
|
213
250
|
assert entry is not None
|
|
214
251
|
assert entry.type == "user"
|
|
215
252
|
assert entry.name == "Prefer typing"
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
# ── Agent-facing memory_write tool ────────────────────────────────────
|
|
256
|
+
|
|
257
|
+
def test_memory_write_tool_persists_and_is_queryable(project_root, memory_base, monkeypatch):
|
|
258
|
+
"""memory_write is the agent's direct way to save a fact; memory_search must find it."""
|
|
259
|
+
import aru.tools.memory_tool as mt
|
|
260
|
+
# Pin the tool's internal resolver to our tmp project_root + base so the
|
|
261
|
+
# write lands in the test-scoped directory rather than ~/.aru.
|
|
262
|
+
monkeypatch.setattr(mt, "_project_root", lambda: project_root)
|
|
263
|
+
from aru.memory import store as _store
|
|
264
|
+
original_dir = _store.memory_dir_for_project
|
|
265
|
+
|
|
266
|
+
def _scoped_dir(pr, base=None, *, create=False):
|
|
267
|
+
return original_dir(pr, base=memory_base, create=create)
|
|
268
|
+
|
|
269
|
+
monkeypatch.setattr(_store, "memory_dir_for_project", _scoped_dir)
|
|
270
|
+
|
|
271
|
+
result = mt.memory_write(
|
|
272
|
+
name="Prefer pytest",
|
|
273
|
+
body="The project uses pytest exclusively; pick pytest idioms.",
|
|
274
|
+
type="user",
|
|
275
|
+
description="pytest, never unittest",
|
|
276
|
+
)
|
|
277
|
+
assert "Saved memory" in result
|
|
278
|
+
assert "user_prefer_pytest" in result
|
|
279
|
+
|
|
280
|
+
hits = mt.memory_search(query="pytest")
|
|
281
|
+
assert "Prefer pytest" in hits
|
|
282
|
+
assert "pytest idioms" in hits
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def test_memory_write_tool_rejects_invalid_type(project_root, monkeypatch):
|
|
286
|
+
import aru.tools.memory_tool as mt
|
|
287
|
+
monkeypatch.setattr(mt, "_project_root", lambda: project_root)
|
|
288
|
+
out = mt.memory_write(name="X", body="b", type="bogus")
|
|
289
|
+
assert "Invalid memory type" in out
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def test_memory_write_tool_requires_name_and_body(project_root, monkeypatch):
|
|
293
|
+
import aru.tools.memory_tool as mt
|
|
294
|
+
monkeypatch.setattr(mt, "_project_root", lambda: project_root)
|
|
295
|
+
assert "requires both" in mt.memory_write(name="", body="b")
|
|
296
|
+
assert "requires both" in mt.memory_write(name="X", body="")
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Regression tests for Ctrl+C handling in ``run_agent_capture``.
|
|
2
|
+
|
|
3
|
+
The bug: on Python 3.11+, ``asyncio.run`` cancels the main task when the
|
|
4
|
+
user hits Ctrl+C. ``run_agent_capture`` catches the resulting
|
|
5
|
+
``CancelledError`` — but Python leaves the task in a "cancelling" state
|
|
6
|
+
until ``Task.uncancel()`` is called. As a result, the REPL's NEXT await
|
|
7
|
+
(the prompt) would raise ``CancelledError`` immediately, making Ctrl+C
|
|
8
|
+
during an agent turn look like it "exits aru" instead of just aborting
|
|
9
|
+
the turn.
|
|
10
|
+
|
|
11
|
+
These tests cover the narrow contract: after an interrupted turn, the
|
|
12
|
+
task must not be in a cancelling state and the abort signal must be
|
|
13
|
+
cleared.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import asyncio
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
async def _catch_and_recover_like_runner() -> None:
|
|
22
|
+
"""Mirror the runner.py:666 except block in isolation.
|
|
23
|
+
|
|
24
|
+
We don't import ``run_agent_capture`` here because a full mock would
|
|
25
|
+
drag in agno + RichLiveSink + ctx — overkill for testing an invariant
|
|
26
|
+
on three lines. Keeping it isolated is the point: if this pattern
|
|
27
|
+
stays valid, the real runner keeps working too.
|
|
28
|
+
"""
|
|
29
|
+
from aru.runtime import reset_abort
|
|
30
|
+
try:
|
|
31
|
+
await asyncio.sleep(10)
|
|
32
|
+
except (KeyboardInterrupt, asyncio.CancelledError):
|
|
33
|
+
current = asyncio.current_task()
|
|
34
|
+
if current is not None:
|
|
35
|
+
current.uncancel()
|
|
36
|
+
reset_abort()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
async def test_repl_task_survives_mid_turn_cancellation():
|
|
40
|
+
"""After Ctrl+C during a turn, the next await must NOT re-raise."""
|
|
41
|
+
loop = asyncio.get_running_loop()
|
|
42
|
+
task = asyncio.current_task()
|
|
43
|
+
assert task is not None
|
|
44
|
+
loop.call_later(0.01, task.cancel)
|
|
45
|
+
await _catch_and_recover_like_runner()
|
|
46
|
+
# If the fix works, this await completes without re-raising. This is
|
|
47
|
+
# the moral equivalent of the REPL reaching its next prompt.
|
|
48
|
+
await asyncio.sleep(0.01)
|
|
49
|
+
assert task.cancelling() == 0
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
async def test_abort_flag_is_cleared_after_interrupt():
|
|
53
|
+
"""Leftover abort flag would silently short-circuit the next turn."""
|
|
54
|
+
from aru.runtime import abort_current, is_aborted, reset_abort
|
|
55
|
+
|
|
56
|
+
reset_abort()
|
|
57
|
+
assert is_aborted() is False
|
|
58
|
+
|
|
59
|
+
loop = asyncio.get_running_loop()
|
|
60
|
+
task = asyncio.current_task()
|
|
61
|
+
assert task is not None
|
|
62
|
+
|
|
63
|
+
# Set abort during the simulated turn, then cancel to trigger recovery.
|
|
64
|
+
def _trigger():
|
|
65
|
+
abort_current()
|
|
66
|
+
task.cancel()
|
|
67
|
+
|
|
68
|
+
loop.call_later(0.01, _trigger)
|
|
69
|
+
await _catch_and_recover_like_runner()
|
|
70
|
+
assert is_aborted() is False
|
aru_code-0.36.0/aru/__init__.py
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.36.0"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|