drydock-cli 3.0.136__tar.gz → 3.0.138__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.
- {drydock_cli-3.0.136/drydock_cli.egg-info → drydock_cli-3.0.138}/PKG-INFO +1 -1
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/drydock/agent.py +3 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/drydock/config.py +4 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/drydock/graphrag.py +86 -17
- drydock_cli-3.0.138/drydock/trajectory.py +75 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/drydock/tui/app.py +43 -19
- {drydock_cli-3.0.136 → drydock_cli-3.0.138/drydock_cli.egg-info}/PKG-INFO +1 -1
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/drydock_cli.egg-info/SOURCES.txt +2 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/pyproject.toml +1 -1
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/tests/test_graphrag_sqlite.py +31 -0
- drydock_cli-3.0.138/tests/test_trajectory.py +58 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/LICENSE +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/NOTICE +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/README.md +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/drydock/__init__.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/drydock/__main__.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/drydock/advisor.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/drydock/bash_safety.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/drydock/builtin_skills/__init__.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/drydock/builtin_skills/ml-data.md +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/drydock/builtin_skills/ml-debug.md +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/drydock/builtin_skills/ml-finetune.md +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/drydock/builtin_skills/ml-metrics.md +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/drydock/builtin_skills/ml-rl.md +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/drydock/builtin_skills/ml-train.md +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/drydock/builtin_skills/nist-ai-rmf.md +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/drydock/builtin_skills/nist-csf.md +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/drydock/builtin_skills/rmf-categorize.md +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/drydock/builtin_skills/rmf-control.md +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/drydock/builtin_skills/rmf-poam.md +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/drydock/builtin_skills/rmf-review.md +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/drydock/builtin_skills/stig-assess.md +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/drydock/builtin_skills/stig-remediate.md +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/drydock/cci.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/drydock/cli.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/drydock/compaction.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/drydock/detect.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/drydock/events.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/drydock/extract.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/drydock/gittools.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/drydock/guards.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/drydock/loop_detect.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/drydock/mcp.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/drydock/poam.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/drydock/providers.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/drydock/recipes.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/drydock/rmf.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/drydock/rmf_graph.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/drydock/skills.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/drydock/stig.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/drydock/suggest.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/drydock/task_state.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/drydock/tool_registry.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/drydock/tool_result.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/drydock/tools/__init__.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/drydock/tui/__init__.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/drydock/tui/approval.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/drydock/tui/messages.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/drydock/tui/widgets.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/drydock/tuning.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/drydock/verification.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/drydock/web.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/drydock_cli.egg-info/dependency_links.txt +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/drydock_cli.egg-info/entry_points.txt +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/drydock_cli.egg-info/requires.txt +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/drydock_cli.egg-info/top_level.txt +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/setup.cfg +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/tests/test_advisor.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/tests/test_approval.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/tests/test_back_command.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/tests/test_bash_background.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/tests/test_bash_binary_output.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/tests/test_bash_crossplatform.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/tests/test_bash_output_bounding.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/tests/test_bash_process_group.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/tests/test_bash_safety.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/tests/test_bash_sanitize.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/tests/test_bash_shell.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/tests/test_bash_stdin.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/tests/test_bash_stop_partial.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/tests/test_bash_timeout_network.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/tests/test_bash_timeout_param.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/tests/test_cci.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/tests/test_cli_agents.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/tests/test_compact_command.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/tests/test_compaction.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/tests/test_config.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/tests/test_config_migration.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/tests/test_context_limit_config.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/tests/test_detect.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/tests/test_dispatch.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/tests/test_e2e_connected.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/tests/test_edit_replace_all.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/tests/test_empty_response.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/tests/test_events.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/tests/test_extract.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/tests/test_failure_loop.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/tests/test_first_run_setup.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/tests/test_gittools.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/tests/test_glob_edit_edges.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/tests/test_graphify_example.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/tests/test_graphrag.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/tests/test_graphrag_quoted_path.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/tests/test_grep_and_read_robust.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/tests/test_guards_and_tools.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/tests/test_hallucinated_tools.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/tests/test_leaked_tool_call.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/tests/test_loop_detect.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/tests/test_mcp.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/tests/test_oneshot_unreachable.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/tests/test_overthink_interrupt.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/tests/test_plan_autocontinue.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/tests/test_poam.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/tests/test_providers_unreachable.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/tests/test_read_index.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/tests/test_recipes.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/tests/test_repetition_interrupt.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/tests/test_rmf.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/tests/test_rmf_graph.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/tests/test_rmf_stig_graph.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/tests/test_rolling_plan.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/tests/test_runaway_repetition.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/tests/test_screenshot.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/tests/test_server_probe.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/tests/test_skills.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/tests/test_stall_retry.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/tests/test_stig.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/tests/test_stop.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/tests/test_streaming_newlines.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/tests/test_subagent.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/tests/test_suggest.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/tests/test_system_prompt_help.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/tests/test_task_state.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/tests/test_todo.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/tests/test_tool_arg_coercion.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/tests/test_tool_arg_coercion_more.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/tests/test_tool_arg_parsing.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/tests/test_tool_result.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/tests/test_tools_undo.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/tests/test_tui.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/tests/test_tuning.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/tests/test_verification_gate.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/tests/test_viewimage.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/tests/test_vision_input.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/tests/test_web_tools.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/tests/test_windows_shell.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/tests/test_write_content_coerce.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.138}/tests/test_xccdf.py +0 -0
|
@@ -383,6 +383,9 @@ def run(
|
|
|
383
383
|
state.task.phase = "complete"
|
|
384
384
|
_emit(state, "done", phase=state.task.phase, edited=session_has_edited,
|
|
385
385
|
verified=bool(last_verification and last_verification.status == "pass"))
|
|
386
|
+
if config.get("trajectory_file"):
|
|
387
|
+
from drydock import trajectory
|
|
388
|
+
trajectory.record(system_prompt, state, config)
|
|
386
389
|
break
|
|
387
390
|
|
|
388
391
|
# Execute each tool call
|
|
@@ -55,6 +55,10 @@ DEFAULTS: dict[str, object] = {
|
|
|
55
55
|
# Durable per-session execution trace (events.py) written as JSONL to
|
|
56
56
|
# ~/.drydock/events/ — inspect/diagnose/replay a task. Set false to disable.
|
|
57
57
|
"event_log": True,
|
|
58
|
+
# Verified-trajectory export (RSI training-data collection). When set to a path,
|
|
59
|
+
# drydock writes the full task trajectory (system prompt + transcript) there at
|
|
60
|
+
# task end; a benchmark harness keeps only verifier-passing ones. Empty = off.
|
|
61
|
+
"trajectory_file": "",
|
|
58
62
|
"theme": "harbor",
|
|
59
63
|
# Optional SECOND model ("advisor") for a stronger second opinion — e.g. a
|
|
60
64
|
# Gemini OpenAI-compatible endpoint on another box. Empty = disabled. The
|
|
@@ -270,16 +270,16 @@ def _iter_text_files(paths: list[str]):
|
|
|
270
270
|
yield fp
|
|
271
271
|
|
|
272
272
|
|
|
273
|
-
def
|
|
274
|
-
"""
|
|
275
|
-
|
|
276
|
-
|
|
273
|
+
def _iter_file_chunks(paths, cwd, skip_sources, progress=None):
|
|
274
|
+
"""Yield (rel_source, [(body, entities), …]) for each ingestible file, one file
|
|
275
|
+
at a time (so callers never hold every file's text at once). Per-file isolation:
|
|
276
|
+
one unreadable/odd file (a locked Word doc, weird encoding, PDF quirk) is skipped,
|
|
277
|
+
not fatal. Calls progress(files_done, rel) after each file so a long build can
|
|
278
|
+
report instead of appearing frozen."""
|
|
277
279
|
cleaned = [_unquote(p) for p in paths]
|
|
280
|
+
files_done = 0
|
|
278
281
|
for fp in _iter_text_files([str(Path(cwd) / p) if not os.path.isabs(p) else p
|
|
279
282
|
for p in cleaned]):
|
|
280
|
-
# Per-file isolation: ONE unreadable/odd file (a locked Word doc, a weird
|
|
281
|
-
# encoding, a PDF backend quirk) must never crash a whole-folder build —
|
|
282
|
-
# skip it and keep indexing the rest.
|
|
283
283
|
try:
|
|
284
284
|
rel = os.path.relpath(str(fp), cwd)
|
|
285
285
|
if rel in skip_sources:
|
|
@@ -289,14 +289,25 @@ def _ingest_files(paths, cwd, chunks, entity_chunks, edges, skip_sources):
|
|
|
289
289
|
else:
|
|
290
290
|
text = fp.read_text("utf-8", "ignore")
|
|
291
291
|
if not text or not text.strip():
|
|
292
|
-
continue
|
|
293
|
-
# Build this file's chunks locally; commit only if it ALL succeeds so a
|
|
294
|
-
# mid-file failure leaves no partial state and isn't miscounted.
|
|
292
|
+
continue
|
|
295
293
|
local = [(body, extract_entities(body)) for body in _chunk_text(text)]
|
|
296
294
|
except Exception: # noqa: BLE001 — isolate a bad file, never abort the build
|
|
297
295
|
continue
|
|
296
|
+
skip_sources.add(rel)
|
|
297
|
+
files_done += 1
|
|
298
|
+
if progress:
|
|
299
|
+
try:
|
|
300
|
+
progress(files_done, rel)
|
|
301
|
+
except Exception: # noqa: BLE001 — a progress callback must never break a build
|
|
302
|
+
pass
|
|
303
|
+
yield rel, local
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def _ingest_files(paths, cwd, chunks, entity_chunks, edges, skip_sources, progress=None):
|
|
307
|
+
"""In-memory ingest into the accumulators (legacy JSON path — small indexes)."""
|
|
308
|
+
added = 0
|
|
309
|
+
for rel, local in _iter_file_chunks(paths, cwd, skip_sources, progress):
|
|
298
310
|
added += 1
|
|
299
|
-
skip_sources.add(rel) # don't double-ingest the same file in one call
|
|
300
311
|
for body, ents in local:
|
|
301
312
|
cid = len(chunks)
|
|
302
313
|
chunks.append({"id": cid, "source": rel, "text": body, "entities": ents})
|
|
@@ -309,6 +320,56 @@ def _ingest_files(paths, cwd, chunks, entity_chunks, edges, skip_sources):
|
|
|
309
320
|
return added
|
|
310
321
|
|
|
311
322
|
|
|
323
|
+
def _build_sqlite_stream(paths, store_path, cwd, skip_sources, progress=None, append=False):
|
|
324
|
+
"""Streaming SQLite build: chunk text is inserted per-file (never held for the
|
|
325
|
+
whole corpus, so memory stays bounded even for a huge folder). Entities/edges
|
|
326
|
+
aggregate in memory (far smaller than the text) and are written at the end."""
|
|
327
|
+
sp = Path(store_path)
|
|
328
|
+
sp.parent.mkdir(parents=True, exist_ok=True)
|
|
329
|
+
if not append and sp.exists():
|
|
330
|
+
sp.unlink()
|
|
331
|
+
con = sqlite3.connect(str(sp))
|
|
332
|
+
entity_chunks: dict[str, set[int]] = defaultdict(set)
|
|
333
|
+
edges: dict[str, Counter] = defaultdict(Counter)
|
|
334
|
+
added = 0
|
|
335
|
+
try:
|
|
336
|
+
con.executescript(_SCHEMA)
|
|
337
|
+
# continue chunk ids after any existing rows (append mode)
|
|
338
|
+
row = con.execute("SELECT COALESCE(MAX(id), -1) FROM chunks").fetchone()
|
|
339
|
+
next_id = (row[0] if row else -1) + 1
|
|
340
|
+
if append: # seed the in-memory graph with what's already stored
|
|
341
|
+
for r in con.execute("SELECT entity, chunk_id FROM entities"):
|
|
342
|
+
entity_chunks[r[0]].add(r[1])
|
|
343
|
+
for r in con.execute("SELECT a, b, weight FROM edges"):
|
|
344
|
+
edges[r[0]][r[1]] = r[2]
|
|
345
|
+
con.execute("DELETE FROM entities"); con.execute("DELETE FROM edges")
|
|
346
|
+
for rel, local in _iter_file_chunks(paths, cwd, skip_sources, progress):
|
|
347
|
+
added += 1
|
|
348
|
+
rows = []
|
|
349
|
+
for body, ents in local:
|
|
350
|
+
cid = next_id; next_id += 1
|
|
351
|
+
con.execute("INSERT INTO chunks(id,source,text) VALUES(?,?,?)", (cid, rel, body))
|
|
352
|
+
con.execute("INSERT INTO chunks_fts(rowid,text) VALUES(?,?)", (cid, body))
|
|
353
|
+
for e in ents:
|
|
354
|
+
entity_chunks[e].add(cid)
|
|
355
|
+
for i, a in enumerate(ents):
|
|
356
|
+
for b in ents[i + 1:]:
|
|
357
|
+
edges[a][b] += 1; edges[b][a] += 1
|
|
358
|
+
rows.append(cid)
|
|
359
|
+
if added % 200 == 0:
|
|
360
|
+
con.commit()
|
|
361
|
+
con.executemany("INSERT INTO entities(entity,chunk_id) VALUES(?,?)",
|
|
362
|
+
((e, cid) for e, cids in entity_chunks.items() for cid in cids))
|
|
363
|
+
con.executemany("INSERT INTO edges(a,b,weight) VALUES(?,?,?)",
|
|
364
|
+
((a, b, w) for a, nbrs in edges.items() for b, w in nbrs.items()))
|
|
365
|
+
con.execute("INSERT OR REPLACE INTO meta(key,value) VALUES('version','2')")
|
|
366
|
+
con.commit()
|
|
367
|
+
return {"files": added, "chunks": next_id, "entities": len(entity_chunks),
|
|
368
|
+
"edges": sum(len(n) for n in edges.values()) // 2}
|
|
369
|
+
finally:
|
|
370
|
+
con.close()
|
|
371
|
+
|
|
372
|
+
|
|
312
373
|
def _save(store_path, chunks, entity_chunks, edges, files_added):
|
|
313
374
|
# SQLite by default (scalable). A caller can still target a .json path
|
|
314
375
|
# explicitly (small/portable indexes, tests) and get the legacy format.
|
|
@@ -334,13 +395,17 @@ def _save(store_path, chunks, entity_chunks, edges, files_added):
|
|
|
334
395
|
}
|
|
335
396
|
|
|
336
397
|
|
|
337
|
-
def build_index(paths: list[str], store_path: str | Path, *, cwd: str = "."
|
|
398
|
+
def build_index(paths: list[str], store_path: str | Path, *, cwd: str = ".",
|
|
399
|
+
progress=None) -> dict:
|
|
338
400
|
"""Build (or REBUILD from scratch) the knowledge graph from paths; persist to
|
|
339
|
-
store_path. Returns {files, chunks, entities, edges}.
|
|
401
|
+
store_path. Returns {files, chunks, entities, edges}. `progress(files_done, src)`
|
|
402
|
+
is called per file (for a UI). SQLite stores stream to disk (bounded memory)."""
|
|
403
|
+
if _is_sqlite(store_path):
|
|
404
|
+
return _build_sqlite_stream(paths, store_path, cwd, set(), progress, append=False)
|
|
340
405
|
chunks: list[dict] = []
|
|
341
406
|
entity_chunks: dict[str, set[int]] = defaultdict(set)
|
|
342
407
|
edges: dict[str, Counter] = defaultdict(Counter)
|
|
343
|
-
added = _ingest_files(paths, cwd, chunks, entity_chunks, edges, set())
|
|
408
|
+
added = _ingest_files(paths, cwd, chunks, entity_chunks, edges, set(), progress)
|
|
344
409
|
return _save(store_path, chunks, entity_chunks, edges, added)
|
|
345
410
|
|
|
346
411
|
|
|
@@ -366,13 +431,17 @@ def _read_all(store_path) -> dict:
|
|
|
366
431
|
return json.loads(Path(sp).read_text("utf-8"))
|
|
367
432
|
|
|
368
433
|
|
|
369
|
-
def add_to_index(paths: list[str], store_path: str | Path, *, cwd: str = "."
|
|
434
|
+
def add_to_index(paths: list[str], store_path: str | Path, *, cwd: str = ".",
|
|
435
|
+
progress=None) -> dict:
|
|
370
436
|
"""Incrementally ADD documents to an existing index (build it if none yet).
|
|
371
437
|
Files already indexed (by relative path) are skipped — clear+build to refresh
|
|
372
438
|
changed files. Returns {files (added), chunks, entities, edges} totals."""
|
|
373
439
|
sp = _resolve_store(store_path)
|
|
374
440
|
if not sp.exists():
|
|
375
|
-
return build_index(paths, store_path, cwd=cwd)
|
|
441
|
+
return build_index(paths, store_path, cwd=cwd, progress=progress)
|
|
442
|
+
if _is_sqlite(sp):
|
|
443
|
+
skip = {r[0] for r in sqlite3.connect(str(sp)).execute("SELECT DISTINCT source FROM chunks")}
|
|
444
|
+
return _build_sqlite_stream(paths, sp, cwd, skip, progress, append=True)
|
|
376
445
|
existing = _read_all(sp)
|
|
377
446
|
chunks: list[dict] = list(existing.get("chunks", []))
|
|
378
447
|
entity_chunks: dict[str, set[int]] = defaultdict(set)
|
|
@@ -382,7 +451,7 @@ def add_to_index(paths: list[str], store_path: str | Path, *, cwd: str = ".") ->
|
|
|
382
451
|
for a, nbrs in existing.get("edges", {}).items():
|
|
383
452
|
edges[a] = Counter(nbrs)
|
|
384
453
|
skip = {c["source"] for c in chunks}
|
|
385
|
-
added = _ingest_files(paths, cwd, chunks, entity_chunks, edges, skip)
|
|
454
|
+
added = _ingest_files(paths, cwd, chunks, entity_chunks, edges, skip, progress)
|
|
386
455
|
return _save(sp, chunks, entity_chunks, edges, added)
|
|
387
456
|
|
|
388
457
|
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Verified-trajectory export — the training-data side of the RSI loop.
|
|
2
|
+
|
|
3
|
+
When `trajectory_file` is set (a benchmark harness sets it per task), drydock writes
|
|
4
|
+
the FULL task trajectory — system prompt + the complete message transcript (every
|
|
5
|
+
assistant turn with its tool calls, every tool result) — to that file at task end.
|
|
6
|
+
The harness then keeps ONLY the trajectories the deterministic verifier confirmed
|
|
7
|
+
correct, so the resulting corpus is solutions that provably work (no self-grading).
|
|
8
|
+
|
|
9
|
+
That corpus is the seed for self-distillation and the same format a stronger teacher
|
|
10
|
+
model feeds into. Export is opt-in and fully defensive — it must never affect a run.
|
|
11
|
+
All logic original to Drydock.
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
_MAX_FIELD = 200_000 # cap any single message body so one huge tool dump can't bloat the record
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _clip(v):
|
|
22
|
+
if isinstance(v, str) and len(v) > _MAX_FIELD:
|
|
23
|
+
return v[:_MAX_FIELD] + f"\n[… {len(v) - _MAX_FIELD} chars truncated]"
|
|
24
|
+
return v
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _sanitize(messages: list) -> list:
|
|
28
|
+
"""A JSON-safe copy of the transcript with oversized bodies clipped."""
|
|
29
|
+
out = []
|
|
30
|
+
for m in messages:
|
|
31
|
+
if not isinstance(m, dict):
|
|
32
|
+
continue
|
|
33
|
+
mm = {k: _clip(v) for k, v in m.items()}
|
|
34
|
+
out.append(mm)
|
|
35
|
+
return out
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def build_record(system_prompt: str, state, config: dict, meta: dict | None = None) -> dict:
|
|
39
|
+
"""Assemble the trajectory record (does not write). Verified/reward are left None
|
|
40
|
+
for the harness to stamp after the verifier runs."""
|
|
41
|
+
task = getattr(state, "task", None)
|
|
42
|
+
tools = sorted({m.get("name", "") for m in state.messages
|
|
43
|
+
if isinstance(m, dict) and m.get("role") == "tool" and m.get("name")})
|
|
44
|
+
return {
|
|
45
|
+
"task": (meta or {}).get("task", ""),
|
|
46
|
+
"model": config.get("model", ""),
|
|
47
|
+
"objective": getattr(task, "objective", "") if task else "",
|
|
48
|
+
"acceptance_criteria": list(getattr(task, "acceptance_criteria", []) or []) if task else [],
|
|
49
|
+
"phase": getattr(task, "phase", "") if task else "",
|
|
50
|
+
"system": system_prompt,
|
|
51
|
+
"messages": _sanitize(state.messages),
|
|
52
|
+
"n_messages": len(state.messages),
|
|
53
|
+
"tools": tools,
|
|
54
|
+
"in_tokens": getattr(state, "total_input_tokens", 0),
|
|
55
|
+
"out_tokens": getattr(state, "total_output_tokens", 0),
|
|
56
|
+
"verified": None, # stamped True by the collector iff the verifier passed
|
|
57
|
+
"reward": None,
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def record(system_prompt: str, state, config: dict, meta: dict | None = None):
|
|
62
|
+
"""Write the current full trajectory to config['trajectory_file'] (overwrite, so
|
|
63
|
+
it always holds the latest complete transcript). No-op if unset. Never raises."""
|
|
64
|
+
path = config.get("trajectory_file")
|
|
65
|
+
if not path:
|
|
66
|
+
return None
|
|
67
|
+
try:
|
|
68
|
+
p = Path(path)
|
|
69
|
+
if p.parent and not p.parent.exists():
|
|
70
|
+
p.parent.mkdir(parents=True, exist_ok=True)
|
|
71
|
+
p.write_text(json.dumps(build_record(system_prompt, state, config, meta), default=str),
|
|
72
|
+
encoding="utf-8")
|
|
73
|
+
return path
|
|
74
|
+
except Exception: # noqa: BLE001 — export must never break a run
|
|
75
|
+
return None
|
|
@@ -1069,6 +1069,44 @@ class DrydockApp(App):
|
|
|
1069
1069
|
"(recent turns are always preserved)."
|
|
1070
1070
|
)
|
|
1071
1071
|
|
|
1072
|
+
def _graphrag_build(self, sub: str, rest: str, store, cwd: str) -> None:
|
|
1073
|
+
"""Worker: build/add a knowledge base off the UI thread, streaming a
|
|
1074
|
+
throttled progress line so a huge folder never looks frozen."""
|
|
1075
|
+
from drydock import graphrag
|
|
1076
|
+
import time as _t
|
|
1077
|
+
|
|
1078
|
+
last = [0.0]
|
|
1079
|
+
|
|
1080
|
+
def progress(files_done: int, src: str) -> None:
|
|
1081
|
+
now = _t.monotonic()
|
|
1082
|
+
if now - last[0] >= 1.0: # throttle UI updates to ~1/s
|
|
1083
|
+
last[0] = now
|
|
1084
|
+
self.call_from_thread(
|
|
1085
|
+
self.query_one("#working", Static).update,
|
|
1086
|
+
f" ⚓ indexing… {files_done} files ({src[-48:]})")
|
|
1087
|
+
|
|
1088
|
+
try:
|
|
1089
|
+
fn = graphrag.build_index if sub == "build" else graphrag.add_to_index
|
|
1090
|
+
stats = fn([rest], store, cwd=cwd, progress=progress)
|
|
1091
|
+
except Exception as e: # noqa: BLE001 — surface, never crash the TUI
|
|
1092
|
+
self.call_from_thread(self.query_one("#working", Static).update, "")
|
|
1093
|
+
self.call_from_thread(self._mount, ErrorMessage(f"graphrag {sub} failed: {e}"))
|
|
1094
|
+
return
|
|
1095
|
+
self.call_from_thread(self.query_one("#working", Static).update, "")
|
|
1096
|
+
if sub == "add" and stats["files"] == 0:
|
|
1097
|
+
self.call_from_thread(
|
|
1098
|
+
self._info, f"No new documents under {rest} (already indexed, or no text found).")
|
|
1099
|
+
return
|
|
1100
|
+
if not stats["chunks"]:
|
|
1101
|
+
self.call_from_thread(self._info, f"No text found under {rest}. Nothing was indexed.")
|
|
1102
|
+
return
|
|
1103
|
+
verb2 = "built" if sub == "build" else f"updated (+{stats['files']} new files)"
|
|
1104
|
+
self.call_from_thread(
|
|
1105
|
+
self._info,
|
|
1106
|
+
f"✓ Knowledge base {verb2}: {stats['chunks']} chunks · "
|
|
1107
|
+
f"{stats['entities']} entities · {stats['edges']} edges.\n"
|
|
1108
|
+
f"Stored at {store}. The agent draws on it via the Knowledge tool.")
|
|
1109
|
+
|
|
1072
1110
|
def _cmd_graphrag(self, arg: str) -> None:
|
|
1073
1111
|
"""Build / inspect / clear the project's GraphRAG knowledge base. Once
|
|
1074
1112
|
built, the agent retrieves from it via the read-only Knowledge tool."""
|
|
@@ -1085,25 +1123,11 @@ class DrydockApp(App):
|
|
|
1085
1123
|
self._info(f"usage: /graphrag {sub} <path> (a file or directory of docs/code)")
|
|
1086
1124
|
return
|
|
1087
1125
|
verb = "Rebuilding" if sub == "build" else "Ingesting into"
|
|
1088
|
-
self._info(f"{verb} knowledge base from {rest} …"
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
self._mount(ErrorMessage(f"graphrag {sub} failed: {e}"))
|
|
1094
|
-
return
|
|
1095
|
-
if sub == "add" and stats["files"] == 0:
|
|
1096
|
-
self._info(f"No new documents under {rest} (already indexed, or no text found).")
|
|
1097
|
-
return
|
|
1098
|
-
if not stats["chunks"]:
|
|
1099
|
-
self._info(f"No text found under {rest}. Nothing was indexed.")
|
|
1100
|
-
return
|
|
1101
|
-
verb2 = "built" if sub == "build" else f"updated (+{stats['files']} new files)"
|
|
1102
|
-
self._info(
|
|
1103
|
-
f"✓ Knowledge base {verb2}: {stats['chunks']} chunks · "
|
|
1104
|
-
f"{stats['entities']} entities · {stats['edges']} edges.\n"
|
|
1105
|
-
f"Stored at {store}. The agent draws on it via the Knowledge tool."
|
|
1106
|
-
)
|
|
1126
|
+
self._info(f"{verb} knowledge base from {rest} … (runs in the background; "
|
|
1127
|
+
"progress below — the TUI stays responsive)")
|
|
1128
|
+
# Run OFF the UI thread so a large folder can't freeze the interface,
|
|
1129
|
+
# and stream progress (files indexed) back so it never looks locked up.
|
|
1130
|
+
self.run_worker(lambda: self._graphrag_build(sub, rest, store, cwd), thread=True)
|
|
1107
1131
|
elif sub == "query":
|
|
1108
1132
|
if not rest:
|
|
1109
1133
|
self._info("usage: /graphrag query <question> (test what the KB returns)")
|
|
@@ -30,6 +30,7 @@ drydock/suggest.py
|
|
|
30
30
|
drydock/task_state.py
|
|
31
31
|
drydock/tool_registry.py
|
|
32
32
|
drydock/tool_result.py
|
|
33
|
+
drydock/trajectory.py
|
|
33
34
|
drydock/tuning.py
|
|
34
35
|
drydock/verification.py
|
|
35
36
|
drydock/web.py
|
|
@@ -133,6 +134,7 @@ tests/test_tool_arg_coercion_more.py
|
|
|
133
134
|
tests/test_tool_arg_parsing.py
|
|
134
135
|
tests/test_tool_result.py
|
|
135
136
|
tests/test_tools_undo.py
|
|
137
|
+
tests/test_trajectory.py
|
|
136
138
|
tests/test_tui.py
|
|
137
139
|
tests/test_tuning.py
|
|
138
140
|
tests/test_verification_gate.py
|
|
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
|
|
|
7
7
|
# PyPI distribution name is drydock-cli (the established install name, continued
|
|
8
8
|
# from the retired v2 fork); the import package + CLI command stay `drydock`.
|
|
9
9
|
name = "drydock-cli"
|
|
10
|
-
version = "3.0.
|
|
10
|
+
version = "3.0.138"
|
|
11
11
|
description = "Drydock — a local, provider-agnostic terminal coding agent for local LLMs"
|
|
12
12
|
readme = "README.md"
|
|
13
13
|
requires-python = ">=3.11"
|
|
@@ -68,3 +68,34 @@ def test_resolve_prefers_db_then_json():
|
|
|
68
68
|
|
|
69
69
|
def test_missing_store_returns_none():
|
|
70
70
|
assert g.load_index("/no/such/graphrag.db") is None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_streaming_build_reports_progress():
|
|
74
|
+
"""SQLite build streams to disk and calls progress per file (so the UI can show
|
|
75
|
+
it and never looks frozen)."""
|
|
76
|
+
d, docs = _docs()
|
|
77
|
+
open(os.path.join(docs, "c.md"), "w").write("# More\nExtra content about billing.")
|
|
78
|
+
seen = []
|
|
79
|
+
store = os.path.join(d, ".drydock", "graphrag.db")
|
|
80
|
+
stats = g.build_index([docs], store, cwd=d, progress=lambda n, s: seen.append((n, s)))
|
|
81
|
+
assert stats["files"] == 3 and len(seen) == 3
|
|
82
|
+
assert seen[-1][0] == 3 # last progress reports 3 files done
|
|
83
|
+
# index is queryable
|
|
84
|
+
assert g.query_index(g.load_index(store), "billing", k=2)["chunks"]
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def test_streaming_append_skips_existing_and_reports():
|
|
88
|
+
d, docs = _docs()
|
|
89
|
+
store = os.path.join(d, ".drydock", "graphrag.db")
|
|
90
|
+
g.build_index([docs], store, cwd=d)
|
|
91
|
+
open(os.path.join(docs, "n.md"), "w").write("# New\nThe RefundBot processes claims.")
|
|
92
|
+
seen = []
|
|
93
|
+
s2 = g.add_to_index([docs], store, cwd=d, progress=lambda n, s: seen.append(s))
|
|
94
|
+
assert s2["files"] == 1 and seen == ["docs/n.md"] # only the new file ingested
|
|
95
|
+
assert g.query_index(g.load_index(store), "refundbot claims", k=2)["chunks"]
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def test_build_without_progress_is_backward_compatible():
|
|
99
|
+
d, docs = _docs()
|
|
100
|
+
store = os.path.join(d, ".drydock", "graphrag.db")
|
|
101
|
+
assert g.build_index([docs], store, cwd=d)["files"] == 2 # no progress arg → still works
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Verified-trajectory export (RSI training-data collection): drydock writes the
|
|
2
|
+
full task transcript when trajectory_file is set, and does nothing otherwise."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import tempfile
|
|
8
|
+
|
|
9
|
+
from drydock import agent, trajectory
|
|
10
|
+
from drydock.providers import AssistantTurn
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _run(cfg, msg="Do the task.\n1. a criterion"):
|
|
14
|
+
turns = [
|
|
15
|
+
AssistantTurn("", [{"id": "1", "name": "Write",
|
|
16
|
+
"input": {"file_path": "/tmp/t.py", "content": "x"}}], 10, 5),
|
|
17
|
+
AssistantTurn("", [{"id": "2", "name": "Bash", "input": {"command": "python /tmp/t.py"}}], 8, 4),
|
|
18
|
+
AssistantTurn("done, verified", [], 6, 3),
|
|
19
|
+
]
|
|
20
|
+
i = {"n": 0}
|
|
21
|
+
def fake(model, system, messages, tool_schemas, config):
|
|
22
|
+
t = turns[min(i["n"], 2)]; i["n"] += 1; yield t
|
|
23
|
+
orig = agent.stream; agent.stream = fake
|
|
24
|
+
try:
|
|
25
|
+
st = agent.AgentState()
|
|
26
|
+
list(agent.run(msg, st, cfg, "SYSTEM"))
|
|
27
|
+
return st
|
|
28
|
+
finally:
|
|
29
|
+
agent.stream = orig
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_exports_full_trajectory_when_enabled():
|
|
33
|
+
tf = os.path.join(tempfile.mkdtemp(), "traj.json")
|
|
34
|
+
cfg = {"model": "gemma4", "context_limit": 65536, "max_tokens": 8192, "trajectory_file": tf}
|
|
35
|
+
_run(cfg)
|
|
36
|
+
rec = json.load(open(tf))
|
|
37
|
+
assert rec["system"] == "SYSTEM" or rec["system"].startswith("SYSTEM")
|
|
38
|
+
assert rec["objective"].startswith("Do the task")
|
|
39
|
+
assert rec["n_messages"] >= 4 and set(rec["tools"]) == {"Bash", "Write"}
|
|
40
|
+
assert rec["verified"] is None and rec["reward"] is None # collector stamps these
|
|
41
|
+
assert [m["role"] for m in rec["messages"]][0] == "user"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_off_by_default_no_file_no_crash():
|
|
45
|
+
st = _run({"model": "gemma4", "context_limit": 65536, "max_tokens": 8192})
|
|
46
|
+
assert st.messages # ran fine; nothing exported
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_record_never_raises_on_bad_path():
|
|
50
|
+
st = agent.AgentState(); st.messages = [{"role": "user", "content": "x"}]
|
|
51
|
+
assert trajectory.record("s", st, {"trajectory_file": "/root/no/perm/x.json"}) is None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_build_record_clips_huge_bodies():
|
|
55
|
+
st = agent.AgentState()
|
|
56
|
+
st.messages = [{"role": "tool", "name": "Bash", "content": "A" * 500_000}]
|
|
57
|
+
rec = trajectory.build_record("s", st, {"model": "m"})
|
|
58
|
+
assert len(rec["messages"][0]["content"]) < 250_000 and "truncated" in rec["messages"][0]["content"]
|
|
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
|