Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion desloppify/app/commands/helpers/command_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@

from desloppify.app.commands.helpers.state import state_path
from desloppify.base.config import load_config
from desloppify.base.runtime_state import current_runtime_context
from desloppify.engine.plan_state import resolve_plan_path_for_state
from desloppify.state_io import StateModel, load_state


Expand All @@ -21,10 +23,20 @@ class CommandRuntime:
state_path: Path | None


def _bind_plan_file(runtime: CommandRuntime) -> None:
"""Keep implicit plan I/O aligned with the selected state file."""
current_runtime_context().plan_file = (
resolve_plan_path_for_state(runtime.state_path, migrate_legacy=True)
if runtime.state_path is not None
else None
)


def command_runtime(args: argparse.Namespace) -> CommandRuntime:
"""Return runtime context from explicit args.runtime or construct one."""
runtime = getattr(args, "runtime", None)
if isinstance(runtime, CommandRuntime):
_bind_plan_file(runtime)
return runtime

config = load_config()
Expand All @@ -34,7 +46,9 @@ def command_runtime(args: argparse.Namespace) -> CommandRuntime:

state = load_state(state_file)

return CommandRuntime(config=config, state=state, state_path=state_file)
runtime = CommandRuntime(config=config, state=state, state_path=state_file)
_bind_plan_file(runtime)
return runtime


__all__ = ["CommandRuntime", "command_runtime"]
16 changes: 14 additions & 2 deletions desloppify/app/commands/helpers/lang.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,9 +92,9 @@ def resolve_detection_root(
"""Best root to auto-detect language from."""
marker_provider = marker_provider or _lang_config_markers
markers = marker_provider()
project_root_path = (
project_root_path = Path(
project_root if project_root is not None else get_project_root()
)
).resolve()

raw_path = getattr(args, "path", None)
if not raw_path:
Expand All @@ -109,11 +109,23 @@ def resolve_detection_root(
for probe_root in (candidate_root, *candidate_root.parents):
if any((probe_root / marker).exists() for marker in markers):
return probe_root
if probe_root == project_root_path or (probe_root / ".git").exists():
break
return candidate_root


def auto_detect_lang_name(args: object) -> str | None:
"""Auto-detect language using the most relevant root for this command."""
state_value = getattr(args, "state", None)
if state_value:
state_file = Path(state_value)
state_parent = state_file.parent.name
if state_parent in lang_api.available_langs():
return state_parent
if state_file.name.startswith("state-") and state_file.suffix == ".json":
state_language = state_file.stem.removeprefix("state-")
if state_language in lang_api.available_langs():
return state_language
root = resolve_detection_root(args)
detected = lang_api.auto_detect_lang(root)
if detected is None and root != get_project_root():
Expand Down
4 changes: 2 additions & 2 deletions desloppify/app/commands/plan/repair_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@

from desloppify.app.commands.helpers.command_runtime import command_runtime
from desloppify.base.output.terminal import colorize
from desloppify.engine.plan_state import load_plan, plan_path_for_state
from desloppify.engine._state.recovery import (
has_saved_plan_without_scan,
reconcile_saved_plan_skips,
reconstruct_state_from_saved_plan,
)
from desloppify.engine.plan_state import load_plan, resolve_plan_path_for_state
from desloppify.state_io import (
StateModel,
empty_state,
Expand All @@ -35,7 +35,7 @@ def cmd_plan_repair_state(args: argparse.Namespace) -> None:
"""Rebuild persisted state from live plan metadata when scan data is gone."""
runtime = command_runtime(args)
state_file = _resolved_state_file(runtime)
plan_path = plan_path_for_state(state_file)
plan_path = resolve_plan_path_for_state(state_file)
plan = load_plan(plan_path)

state = runtime.state
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,15 +105,25 @@ def _read_stage_output(output_file: Path) -> str:



def _write_desloppify_cli_helper(run_dir: Path) -> Path:
def _write_desloppify_cli_helper(run_dir: Path, state_path: Path | None = None) -> Path:
"""Create an exact CLI wrapper so codex subagents use this checkout + interpreter."""
package_root = Path(desloppify.__file__).resolve().parent.parent
script_path = run_dir / "run_desloppify.sh"
python_command = f"{shlex.quote(sys.executable)} -m desloppify.cli"
script = (
"#!/bin/sh\n"
f"export PYTHONPATH={shlex.quote(str(package_root))}${{PYTHONPATH:+:$PYTHONPATH}}\n"
f"exec {shlex.quote(sys.executable)} -m desloppify.cli \"$@\"\n"
)
if state_path is None:
script += f'exec {python_command} "$@"\n'
else:
script += (
f'[ "$#" -gt 0 ] || exec {python_command} "$@"\n'
'command_name=$1\n'
'shift\n'
f'exec {python_command} "$command_name" --state '
f'{shlex.quote(str(state_path))} "$@"\n'
)
safe_write_text(script_path, script)
os.chmod(script_path, 0o700)
return script_path
Expand Down Expand Up @@ -383,7 +393,10 @@ def run_codex_pipeline(

run_log_path = run_dir / "run.log"
append_run_log = make_run_log_writer(run_log_path)
cli_helper = _write_desloppify_cli_helper(run_dir)
cli_helper = _write_desloppify_cli_helper(
run_dir,
getattr(runtime, "state_path", None),
)
runner_label = active_runner_name()
append_run_log(
f"run-start runner={runner_label} stages={','.join(stages_to_run)} "
Expand Down
39 changes: 23 additions & 16 deletions desloppify/app/commands/review/batch/orchestrator.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@
from typing import cast

from desloppify.app.commands.helpers.query import write_query_best_effort
from desloppify.app.commands.runner.codex_batch import (
CodexBatchRunnerDeps,
FollowupScanDeps,
run_codex_batch,
run_followup_scan,
)
from desloppify.base.discovery.file_paths import safe_write_text
from desloppify.base.exception_sets import CommandError, PacketValidationError
from desloppify.base.output.terminal import colorize, log
Expand All @@ -33,6 +39,7 @@
from ..packet.policy import redacted_review_config
from ..prompt_sections import explode_to_single_dimension
from ..runner_failures import print_failures, print_failures_and_raise
from ..runner_opencode import run_opencode_batch
from ..runner_packets import (
build_batch_import_provenance,
build_blind_packet,
Expand All @@ -41,14 +48,11 @@
selected_batch_indexes,
write_packet_snapshot,
)
from ..runner_parallel import BatchExecutionOptions, collect_batch_results, execute_batches
from desloppify.app.commands.runner.codex_batch import (
CodexBatchRunnerDeps,
FollowupScanDeps,
run_codex_batch,
run_followup_scan,
from ..runner_parallel import (
BatchExecutionOptions,
collect_batch_results,
execute_batches,
)
from ..runner_opencode import run_opencode_batch
from ..runner_rovodev import run_rovodev_batch
from ..runtime.setup import setup_lang_concrete as _setup_lang
from ..runtime_paths import (
Expand All @@ -63,22 +67,24 @@
from ..runtime_paths import (
subagent_runs_dir as _subagent_runs_dir,
)
from . import execution as review_batches_mod
from . import execution_phases as review_batch_phases_mod
from .core_merge_support import assessment_weight # noqa: F401 — re-exported
from .core_models import BatchResultPayload
from .scope import (
normalize_dimension_list,
scored_dimensions_for_lang,
)
from .core_normalize import normalize_batch_result
from .core_parse import extract_json_payload, parse_batch_selection
from . import execution_phases as review_batch_phases_mod
from .merge import merge_batch_results
from .prompt_template import render_batch_prompt
from . import execution as review_batches_mod
from .execution_results import (
enforce_import_coverage as _enforce_import_coverage,
)
from .execution_results import (
merge_and_write_results as _merge_and_write_results,
)
from .merge import merge_batch_results
from .prompt_template import render_batch_prompt
from .scope import (
normalize_dimension_list,
scored_dimensions_for_lang,
)

FOLLOWUP_SCAN_TIMEOUT_SECONDS = 45 * 60

Expand Down Expand Up @@ -398,7 +404,8 @@ def do_run_batches(args, state, lang, state_file, config: dict | None = None) ->
"""Run holistic investigation batches with a local subagent runner."""
from ..runtime.policy import resolve_batch_run_policy # noqa: PLC0415

project_root = _runtime_project_root()
scan_path = getattr(args, "path", None)
project_root = Path(scan_path).resolve() if scan_path else _runtime_project_root()
subagent_runs_dir = _subagent_runs_dir()
policy = resolve_batch_run_policy(args)
batch_deps = _build_batch_run_deps(
Expand Down
8 changes: 6 additions & 2 deletions desloppify/app/commands/scan/cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@
from pathlib import Path

from desloppify.app.commands.helpers.by_language import detect_present_languages
from desloppify.app.commands.helpers.command_runtime import command_runtime
from desloppify.app.commands.helpers.lang import resolve_lang
from desloppify.app.commands.helpers.query import query_file_path
from desloppify.app.commands.helpers.runtime_options import (
LangRuntimeOptionsError,
print_lang_runtime_options_error,
)
from desloppify.base.config import target_strict_score_from_config
from desloppify.app.commands.scan.artifacts import (
build_scan_query_payload,
emit_scorecard_badge,
Expand Down Expand Up @@ -46,8 +46,9 @@
resolve_noise_snapshot,
run_scan_generation,
)
from desloppify.base.exception_sets import CommandError
from desloppify.base.config import target_strict_score_from_config
from desloppify.base.discovery.paths import get_project_root
from desloppify.base.exception_sets import CommandError
from desloppify.base.output.terminal import colorize
from desloppify.base.search.query import write_query

Expand Down Expand Up @@ -220,6 +221,9 @@ def _cmd_scan_by_language(args: argparse.Namespace) -> None:
lang_args.by_language = False
lang_args.lang = lang_name
lang_args.state = None
if hasattr(lang_args, "runtime"):
del lang_args.runtime
lang_args.runtime = command_runtime(lang_args)
cmd_scan(lang_args)


Expand Down
1 change: 1 addition & 0 deletions desloppify/base/runtime_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ class RuntimeContext:

exclusions: tuple[str, ...] = ()
project_root: Path | None = None
plan_file: Path | None = None
query_file: Path | None = None
file_text_cache: FileTextCache = field(default_factory=FileTextCache)
cache_enabled: bool = False
Expand Down
31 changes: 16 additions & 15 deletions desloppify/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,17 @@
from typing import Any

from desloppify.app.cli_support.parser import create_parser as _create_parser
from desloppify.app.commands.helpers.command_runtime import (
command_runtime,
)
from desloppify.app.commands.helpers.lang import resolve_lang
from desloppify.app.commands.helpers.command_runtime import CommandRuntime
from desloppify.app.commands.helpers.state import state_path
from desloppify.app.commands.registry import CommandHandler, get_command_handlers
from desloppify.base.config import load_config
from desloppify.base.discovery.paths import get_default_scan_path, get_project_root
from desloppify.base.discovery.source import set_exclusions
from desloppify.base.exception_sets import CommandError
from desloppify.base.output.fallbacks import log_best_effort_failure
from desloppify.base.output.terminal import colorize
from desloppify.base.discovery.paths import get_default_scan_path, get_project_root
from desloppify.base.registry import detector_names, on_detector_registered
from desloppify.base.runtime_state import runtime_scope
from desloppify.languages import available_langs
Expand Down Expand Up @@ -114,12 +115,17 @@ def _project_root_from_state_path(state_path_value: str | Path | None) -> Path |
state_file = Path(state_path_value).resolve()
except OSError:
return None
if state_file.parent.name != ".desloppify":
for parent in state_file.parents:
if parent.name != ".desloppify":
continue
relative = state_file.relative_to(parent)
if len(relative.parts) > 2:
return None
if state_file.name == "state.json" or (
state_file.name.startswith("state-") and state_file.suffix == ".json"
):
return parent.parent
return None
if state_file.name == "state.json" or (
state_file.name.startswith("state-") and state_file.suffix == ".json"
):
return state_file.parent.parent
return None


Expand Down Expand Up @@ -157,13 +163,8 @@ def _resolve_default_path(args: argparse.Namespace) -> None:

def _load_shared_runtime(args: argparse.Namespace) -> None:
"""Load config/state and attach shared objects to parsed args."""
config = load_config()

state_file = state_path(args)
state = load_state(state_file)
_apply_persisted_exclusions(args, config)

args.runtime = CommandRuntime(config=config, state=state, state_path=state_file)
args.runtime = command_runtime(args)
_apply_persisted_exclusions(args, args.runtime.config)


def _looks_like_desloppify_checkout(root: Path) -> bool:
Expand Down
Loading