From 87c156f6396bc79e35555d84ecbbb504f297474f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 4 Apr 2026 08:44:18 +0000 Subject: [PATCH 1/2] feat: add RepoDiagnoser tool with CLI, fix cli.py syntax errors, add copilot-instructions.md Agent-Logs-Url: https://github.com/aidoruao/orthogonal-engineering/sessions/7b62668d-02d4-42be-be6a-4c991db7fb75 Co-authored-by: aidoruao <174227749+aidoruao@users.noreply.github.com> --- .github/copilot-instructions.md | 85 +++++++++++++ cli.py | 153 +++++++++++++++-------- tools/repo_diagnoser/README.md | 93 ++++++++++++++ tools/repo_diagnoser/__init__.py | 11 ++ tools/repo_diagnoser/cli_diagnose.py | 165 ++++++++++++++++++++++++ tools/repo_diagnoser/diagnoser.py | 179 +++++++++++++++++++++++++++ 6 files changed, 631 insertions(+), 55 deletions(-) create mode 100644 .github/copilot-instructions.md create mode 100644 tools/repo_diagnoser/README.md create mode 100644 tools/repo_diagnoser/__init__.py create mode 100644 tools/repo_diagnoser/cli_diagnose.py create mode 100644 tools/repo_diagnoser/diagnoser.py diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 00000000..b8adf088 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,85 @@ +# Copilot Agent Instructions + +## Repository overview + +This is the **Orthogonal Engineering** repository — a deterministic audit and +governance engine. Key primitives live in: + +| Path | Purpose | +|------|---------| +| `toolkit/oe/hasher.py` | SHA-256 file/byte hashing with optional HMAC | +| `toolkit/oe/merkle.py` | Binary Merkle tree, inclusion proofs, JSONL export | +| `minimal_ai_ide/repository_scanner.py` | Structural repository scanner | +| `cli.py` | Main CLI (subcommands: `index`, `merkle`, `handling-clamp`, `verify`, `diagnose`) | +| `tools/repo_diagnoser/` | RepoDiagnoser tool (see below) | + +--- + +## External repository analysis — RepoDiagnoser + +To clone and analyse **any public GitHub repository** from a terminal: + +```bash +# Dry-run (shows what would happen, no writes) +python -m tools.repo_diagnoser.cli_diagnose --url https://github.com/owner/repo + +# Clone and analyse (requires --apply) +python -m tools.repo_diagnoser.cli_diagnose \ + --url https://github.com/owner/repo \ + --apply + +# Full clone, specific branch, export Merkle proofs +python -m tools.repo_diagnoser.cli_diagnose \ + --url https://github.com/owner/repo \ + --depth 0 --ref main \ + --out-proofs /tmp/proofs.jsonl \ + --apply + +# Analyse an already-cloned directory +python -m tools.repo_diagnoser.cli_diagnose --local /tmp/repo_analysis/myrepo + +# Via the main CLI +python cli.py diagnose --url https://github.com/owner/repo --apply +python cli.py diagnose --local /tmp/myrepo --apply +``` + +### Python API + +```python +from tools.repo_diagnoser import RepoDiagnoser + +diagnoser = RepoDiagnoser() +result = diagnoser.diagnose("https://github.com/owner/repo") + +print(result["merkle_root"]) # integrity fingerprint +print(result["file_hashes"]["README.md"]) # per-file SHA-256 +proof = result["tree"].get_inclusion_proof("README.md") +result["tree"].export_proofs_jsonl(Path("/tmp/proofs.jsonl")) +``` + +--- + +## CLI safety contract + +- **Default behaviour is always dry-run** — no files are written without `--apply`. +- Use `--apply` to perform writes (clones, manifests, proof files). +- No credentials are stored or transmitted; only public repositories are supported. + +--- + +## Adding new subcommands to cli.py + +Follow the existing pattern: + +1. Add `cmd_(args) -> int` function. +2. Register the subparser in `main()` with `subparsers.add_parser(...)`. +3. Add `elif args.command == '': return cmd_(args)` to the dispatch block. + +--- + +## Coding conventions + +- No bare `except: pass` — always catch specific exceptions and log them. +- Dry-run by default; `--apply` required for mutations. +- Reuse `toolkit.oe.hasher` and `toolkit.oe.merkle` rather than re-implementing SHA-256 or Merkle logic. +- Imports from this repo use absolute package paths (e.g. `from toolkit.oe.hasher import hash_file`). diff --git a/cli.py b/cli.py index cb4e1d96..788406f7 100644 --- a/cli.py +++ b/cli.py @@ -13,6 +13,7 @@ python cli.py process file.txt --dry-run python cli.py manifest create files/*.txt """ +""" CLI module - Main entrypoint for the deterministic pipeline scaffold. This module provides command-line interface with subcommands: @@ -178,6 +179,7 @@ def cmd_manifest(args): from merkle import MerkleTreeBuilder, verify_inclusion_proof from handling_pipeline import process_handling_file from logger import create_logger +from tools.repo_diagnoser.diagnoser import RepoDiagnoser VERSION = "1.0.0" @@ -368,6 +370,54 @@ def cmd_verify(args): return 0 +def cmd_diagnose(args) -> int: + """Diagnose command — clone and analyse a public Git repository.""" + diagnoser = RepoDiagnoser(clone_dir=args.clone_dir) + + if args.url: + prefix = "DRY RUN — " if not args.apply else "" + print(f"{prefix}Cloning {args.url} …") + if not args.apply: + print(" (pass --apply to perform the clone and analysis)") + return 0 + try: + result = diagnoser.diagnose(args.url, depth=args.depth, ref=args.ref) + except RuntimeError as exc: + print(f"Error: {exc}", file=sys.stderr) + return 1 + else: + local_path = Path(args.local) + if not local_path.is_dir(): + print(f"Error: local path not found: {local_path}", file=sys.stderr) + return 1 + print(f"Analysing local repository: {local_path}") + result = diagnoser.analyze(local_path) + result["repo_path"] = str(local_path) + + if args.out_proofs: + if not args.apply: + print(f"DRY RUN — would write proofs to: {args.out_proofs}") + else: + result["tree"].export_proofs_jsonl(Path(args.out_proofs)) + print(f"Inclusion proofs written to: {args.out_proofs}") + + print("\nDiagnosis complete.") + summary = { + "repo_path": result.get("repo_path", ""), + "merkle_root": result["merkle_root"], + "file_count": len(result["file_hashes"]), + "scan_timestamp": result["scan"].get("scan_timestamp", ""), + } + if args.json: + print(json.dumps(summary, indent=2)) + else: + print(f" Repo path : {summary['repo_path']}") + print(f" Files : {summary['file_count']}") + print(f" Merkle root: {summary['merkle_root']}") + print(f" Scanned at : {summary['scan_timestamp']}") + return 0 + + def main(): """Main CLI entry point.""" parser = argparse.ArgumentParser( @@ -428,37 +478,7 @@ def main(): help="Output path (for create)") manifest_parser.add_argument("--verbose", action="store_true", help="Verbose output") - - description='Deterministic Pipeline Scaffold - Default behavior is DRY-RUN', - formatter_class=argparse.RawDescriptionHelpFormatter, - epilog=""" -Examples: - # Index repository (dry-run) - python cli.py index --repo /path/to/repo - - # Index and create manifest (apply) - python cli.py index --repo /path/to/repo --apply --out manifest.jsonl - - # Build Merkle tree from manifest - python cli.py merkle --manifest manifest.jsonl --apply - - # Process handling file (dry-run) - python cli.py handling-clamp --handling-path handling.meta - - # Process handling file (apply) - python cli.py handling-clamp --handling-path handling.meta --apply --out ./output - - # Verify proofs - python cli.py verify --manifest merkle_proofs.jsonl - -Safety Notes: - - Default behavior is DRY-RUN (no writes) - - Use --apply flag to perform actual writes - - Backups are created automatically before overwrites - - No network calls or credentials used -""" - ) - + parser.add_argument('--version', action='version', version=f'%(prog)s {VERSION}') subparsers = parser.add_subparsers(dest='command', help='Available commands') @@ -486,6 +506,38 @@ def main(): # Verify command verify_parser = subparsers.add_parser('verify', help='Verify manifest or proofs') verify_parser.add_argument('--manifest', type=str, required=True, help='Manifest or proof file to verify') + + # Diagnose command + diagnose_parser = subparsers.add_parser( + 'diagnose', help='Clone and analyse a public Git repository' + ) + diagnose_source = diagnose_parser.add_mutually_exclusive_group(required=True) + diagnose_source.add_argument('--url', metavar='URL', help='Public Git repository URL to clone') + diagnose_source.add_argument( + '--local', metavar='PATH', help='Path to an already-cloned local repository' + ) + diagnose_parser.add_argument( + '--depth', type=int, default=1, metavar='N', + help='Shallow-clone depth (default: 1). Use 0 for a full clone.' + ) + diagnose_parser.add_argument( + '--ref', metavar='REF', default=None, + help='Branch or tag to check out (default: repository default branch)' + ) + diagnose_parser.add_argument( + '--clone-dir', metavar='DIR', default='/tmp/repo_analysis', + help='Base directory for clones (default: /tmp/repo_analysis)' + ) + diagnose_parser.add_argument( + '--out-proofs', metavar='FILE', default=None, + help='Write Merkle inclusion proofs to this JSONL file (requires --apply)' + ) + diagnose_parser.add_argument( + '--apply', action='store_true', help='Apply changes (default is dry-run)' + ) + diagnose_parser.add_argument( + '--json', action='store_true', help='Print summary as JSON' + ) # Parse arguments args = parser.parse_args() @@ -496,19 +548,27 @@ def main(): try: if args.command == "hash": - cmd_hash(args) + return cmd_hash(args) elif args.command == "process": - cmd_process(args) + return cmd_process(args) elif args.command == "backup": - cmd_backup(args) + return cmd_backup(args) elif args.command == "manifest": - cmd_manifest(args) + return cmd_manifest(args) + elif args.command == 'index': + return cmd_index(args) + elif args.command == 'merkle': + return cmd_merkle(args) + elif args.command == 'handling-clamp': + return cmd_handling_clamp(args) + elif args.command == 'verify': + return cmd_verify(args) + elif args.command == 'diagnose': + return cmd_diagnose(args) else: parser.print_help() return 1 - - return 0 - + except Exception as e: logger = get_logger("cli") logger.error(f"Command failed: {e}") @@ -517,22 +577,5 @@ def main(): return 1 -if __name__ == "__main__": - return 1 - - # Dispatch to command handler - if args.command == 'index': - return cmd_index(args) - elif args.command == 'merkle': - return cmd_merkle(args) - elif args.command == 'handling-clamp': - return cmd_handling_clamp(args) - elif args.command == 'verify': - return cmd_verify(args) - else: - parser.print_help() - return 1 - - if __name__ == '__main__': sys.exit(main()) diff --git a/tools/repo_diagnoser/README.md b/tools/repo_diagnoser/README.md new file mode 100644 index 00000000..408d61d5 --- /dev/null +++ b/tools/repo_diagnoser/README.md @@ -0,0 +1,93 @@ +# RepoDiagnoser + +Clone and analyse any public Git repository from the command line or Python API. + +## What it does + +1. **Clones** the target repository (shallow by default; full clone optional). +2. **Scans** its structure using the existing `RepositoryScanner` (detects system types, categorises files, analyses imports and dependencies). +3. **Hashes** every file with `toolkit.oe.hasher.hash_file` (SHA-256). +4. **Builds a Merkle tree** with `toolkit.oe.merkle.MerkleTree` and returns the root hash as a compact fingerprint. +5. Optionally **exports per-file inclusion proofs** as a JSONL file. + +## File layout + +``` +tools/repo_diagnoser/ +├── __init__.py # Exports RepoDiagnoser +├── diagnoser.py # RepoDiagnoser class +├── cli_diagnose.py # Standalone CLI entry point +└── README.md # This file +``` + +## CLI usage + +```bash +# Analyse a remote repo — dry-run (no clone, just shows what would happen) +python -m tools.repo_diagnoser.cli_diagnose --url https://github.com/owner/repo + +# Clone and analyse (--apply to actually perform the clone) +python -m tools.repo_diagnoser.cli_diagnose \ + --url https://github.com/owner/repo \ + --apply + +# Full clone, specific branch, write proofs +python -m tools.repo_diagnoser.cli_diagnose \ + --url https://github.com/owner/repo \ + --depth 0 --ref main \ + --out-proofs proofs.jsonl \ + --apply + +# Analyse an already-cloned local directory +python -m tools.repo_diagnoser.cli_diagnose --local /tmp/repo_analysis/repo + +# Output summary as JSON +python -m tools.repo_diagnoser.cli_diagnose --local /tmp/myrepo --json +``` + +## Via the main cli.py + +```bash +# Dry-run (shows what would be done) +python cli.py diagnose --url https://github.com/owner/repo + +# Apply +python cli.py diagnose --url https://github.com/owner/repo --apply + +# Local path +python cli.py diagnose --local /tmp/myrepo --apply +``` + +## Python API + +```python +from tools.repo_diagnoser import RepoDiagnoser + +diagnoser = RepoDiagnoser() # clones to /tmp/repo_analysis/ +result = diagnoser.diagnose("https://github.com/owner/repo") + +print(result["merkle_root"]) # hex root hash +print(len(result["file_hashes"])) # number of files hashed + +# Inclusion proof for a specific file +proof = result["tree"].get_inclusion_proof("README.md") + +# Export all proofs +result["tree"].export_proofs_jsonl(Path("proofs.jsonl")) +``` + +## Integration points + +| Component | File | Usage | +|-----------|------|-------| +| File hashing | `toolkit/oe/hasher.py` | `hash_file()` — SHA-256 raw bytes | +| Merkle tree | `toolkit/oe/merkle.py` | `MerkleTree.add_leaf()`, `.build()`, `.get_inclusion_proof()`, `.export_proofs_jsonl()` | +| Structural scan | `minimal_ai_ide/repository_scanner.py` | `RepositoryScanner.scan_entire_repository()` | +| Main CLI | `cli.py` | `diagnose` subcommand | + +## Safety notes + +- `--apply` is required for any write operation (clone, proofs file). + Default behaviour is always **dry-run**. +- Clones are placed in `/tmp/repo_analysis` and can be overridden with `--clone-dir`. +- Only public repositories are supported; no credentials are stored or used. diff --git a/tools/repo_diagnoser/__init__.py b/tools/repo_diagnoser/__init__.py new file mode 100644 index 00000000..91cfe68d --- /dev/null +++ b/tools/repo_diagnoser/__init__.py @@ -0,0 +1,11 @@ +""" +RepoDiagnoser — clone and analyse any public Git repository. + +Public API:: + + from tools.repo_diagnoser import RepoDiagnoser +""" + +from .diagnoser import RepoDiagnoser + +__all__ = ["RepoDiagnoser"] diff --git a/tools/repo_diagnoser/cli_diagnose.py b/tools/repo_diagnoser/cli_diagnose.py new file mode 100644 index 00000000..85ac506b --- /dev/null +++ b/tools/repo_diagnoser/cli_diagnose.py @@ -0,0 +1,165 @@ +#!/usr/bin/env python3 +""" +cli_diagnose.py — standalone CLI entry point for RepoDiagnoser. + +Usage +----- + python -m tools.repo_diagnoser.cli_diagnose --url https://github.com/owner/repo + python -m tools.repo_diagnoser.cli_diagnose --url https://github.com/owner/repo --depth 0 + python -m tools.repo_diagnoser.cli_diagnose --url https://github.com/owner/repo --ref main + python -m tools.repo_diagnoser.cli_diagnose --local /path/to/already-cloned-repo + python -m tools.repo_diagnoser.cli_diagnose --url ... --out-proofs proofs.jsonl --apply +""" + +import argparse +import json +import logging +import sys +from pathlib import Path + +from tools.repo_diagnoser.diagnoser import RepoDiagnoser + + +def _build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + prog="cli_diagnose", + description="Clone and analyse a public Git repository.", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + # Analyse a remote repo (shallow clone, dry-run) + python -m tools.repo_diagnoser.cli_diagnose --url https://github.com/owner/repo + + # Full clone, specific branch, write inclusion proofs + python -m tools.repo_diagnoser.cli_diagnose \\ + --url https://github.com/owner/repo --depth 0 --ref main \\ + --out-proofs proofs.jsonl --apply + + # Analyse an already-cloned local directory + python -m tools.repo_diagnoser.cli_diagnose --local /tmp/repo_analysis/repo + +Safety notes: + - Default is DRY-RUN: no files are written unless --apply is passed. + - Clones are placed in /tmp/repo_analysis (override with --clone-dir). +""", + ) + + source = parser.add_mutually_exclusive_group(required=True) + source.add_argument("--url", metavar="URL", help="Public Git repository URL to clone.") + source.add_argument( + "--local", metavar="PATH", help="Path to an already-cloned local repository." + ) + + parser.add_argument( + "--depth", + type=int, + default=1, + metavar="N", + help="Shallow-clone depth (default: 1). Use 0 for a full clone.", + ) + parser.add_argument( + "--ref", + metavar="REF", + default=None, + help="Branch or tag to check out (default: repository default branch).", + ) + parser.add_argument( + "--clone-dir", + metavar="DIR", + default="/tmp/repo_analysis", + help="Base directory for clones (default: /tmp/repo_analysis).", + ) + parser.add_argument( + "--out-proofs", + metavar="FILE", + default=None, + help="Write Merkle inclusion proofs to this JSONL file (requires --apply).", + ) + parser.add_argument( + "--apply", + action="store_true", + default=False, + help="Perform writes (proofs file, etc.). Default is dry-run.", + ) + parser.add_argument( + "--json", + action="store_true", + default=False, + help="Print summary as JSON instead of human-readable text.", + ) + parser.add_argument( + "--verbose", + action="store_true", + default=False, + help="Enable debug logging.", + ) + return parser + + +def _print_summary(result: dict, use_json: bool) -> None: + """Print the analysis summary to stdout.""" + summary = { + "repo_path": result.get("repo_path", ""), + "merkle_root": result["merkle_root"], + "file_count": len(result["file_hashes"]), + "scan_timestamp": result["scan"].get("scan_timestamp", ""), + } + if use_json: + print(json.dumps(summary, indent=2)) + else: + print(f" Repo path : {summary['repo_path']}") + print(f" Files : {summary['file_count']}") + print(f" Merkle root: {summary['merkle_root']}") + print(f" Scanned at : {summary['scan_timestamp']}") + + +def main(argv=None) -> int: + parser = _build_parser() + args = parser.parse_args(argv) + + logging.basicConfig( + level=logging.DEBUG if args.verbose else logging.INFO, + format="%(levelname)s %(name)s: %(message)s", + ) + log = logging.getLogger("cli_diagnose") + + diagnoser = RepoDiagnoser(clone_dir=args.clone_dir) + + # -- obtain repository path ------------------------------------------- + if args.url: + mode = "DRY RUN — " if not args.apply else "" + print(f"{mode}Cloning {args.url} …") + if not args.apply: + print(" (pass --apply to perform the clone and analysis)") + return 0 + try: + result = diagnoser.diagnose(args.url, depth=args.depth, ref=args.ref) + except RuntimeError as exc: + log.error("%s", exc) + return 1 + else: + repo_path = Path(args.local) + if not repo_path.is_dir(): + log.error("Local path does not exist or is not a directory: %s", repo_path) + return 1 + print(f"Analysing local repository: {repo_path}") + result = diagnoser.analyze(repo_path) + result["repo_path"] = str(repo_path) + + # -- optional proofs output ------------------------------------------- + if args.out_proofs: + if not args.apply: + print(f"DRY RUN — would write proofs to: {args.out_proofs}") + else: + proofs_path = Path(args.out_proofs) + result["tree"].export_proofs_jsonl(proofs_path) + print(f"Inclusion proofs written to: {proofs_path}") + + # -- summary ---------------------------------------------------------- + print("\nDiagnosis complete.") + _print_summary(result, use_json=args.json) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/repo_diagnoser/diagnoser.py b/tools/repo_diagnoser/diagnoser.py new file mode 100644 index 00000000..5b6faa6d --- /dev/null +++ b/tools/repo_diagnoser/diagnoser.py @@ -0,0 +1,179 @@ +""" +RepoDiagnoser — clone and analyse any public Git repository. + +Reuses existing toolkit primitives: +- toolkit/oe/hasher.py — hash_file(), hash_bytes_chunked() +- toolkit/oe/merkle.py — MerkleTree +- minimal_ai_ide/repository_scanner.py — RepositoryScanner +""" + +import logging +import shutil +import subprocess +from pathlib import Path +from typing import Dict, Optional + +from toolkit.oe.hasher import hash_file +from toolkit.oe.merkle import MerkleTree +from minimal_ai_ide.repository_scanner import RepositoryScanner + +logger = logging.getLogger(__name__) + + +class RepoDiagnoser: + """Clone and analyse a public Git repository. + + Parameters + ---------- + clone_dir: + Base directory for cloned repositories. + Defaults to ``/tmp/repo_analysis``. + """ + + def __init__(self, clone_dir: str = "/tmp/repo_analysis") -> None: + self.clone_dir = Path(clone_dir) + self.clone_dir.mkdir(parents=True, exist_ok=True) + + # ------------------------------------------------------------------ + # Clone + # ------------------------------------------------------------------ + + def clone_repo( + self, + repo_url: str, + depth: int = 1, + ref: Optional[str] = None, + ) -> Path: + """Clone a public repository. + + Parameters + ---------- + repo_url: + HTTPS or SSH URL of the repository. + depth: + Shallow-clone depth. Pass ``0`` for a full clone (required + when checking out specific commits). + ref: + Branch or tag to check out. ``None`` means the default branch. + + Returns + ------- + Path + Local path to the cloned repository root. + """ + repo_name = repo_url.rstrip("/").split("/")[-1].replace(".git", "") + target = self.clone_dir / repo_name + + if target.exists(): + logger.info("Removing existing clone at %s", target) + shutil.rmtree(target) + + cmd = ["git", "clone"] + if depth > 0: + cmd += ["--depth", str(depth)] + if ref: + cmd += ["--branch", ref] + cmd += [repo_url, str(target)] + + logger.info("Cloning %s → %s", repo_url, target) + try: + subprocess.run(cmd, check=True, capture_output=True, text=True) + except subprocess.CalledProcessError as exc: + raise RuntimeError( + f"git clone failed for {repo_url}: {exc.stderr.strip()}" + ) from exc + + return target + + # ------------------------------------------------------------------ + # Analyse + # ------------------------------------------------------------------ + + def analyze(self, repo_path: Path) -> Dict: + """Analyse a local repository. + + Uses :class:`~minimal_ai_ide.repository_scanner.RepositoryScanner` + for structural scanning, ``toolkit.oe.hasher.hash_file`` for file + hashing, and :class:`~toolkit.oe.merkle.MerkleTree` for fingerprint + generation. + + Parameters + ---------- + repo_path: + Root of the repository to analyse. + + Returns + ------- + dict with keys: + ``scan`` — full RepositoryScanner result dict + ``file_hashes`` — mapping ``{relative_path: sha256_hex}`` + ``merkle_root`` — hex root hash of the Merkle tree + ``tree`` — :class:`~toolkit.oe.merkle.MerkleTree` instance + (can be used to generate per-file inclusion proofs) + """ + repo_path = Path(repo_path) + + # -- structural scan -------------------------------------------------- + logger.info("Running RepositoryScanner on %s", repo_path) + scanner = RepositoryScanner(root_dir=str(repo_path)) + scan_results = scanner.scan_entire_repository() + + # -- hash every file + build Merkle tree ------------------------------ + tree = MerkleTree() + file_hashes: Dict[str, str] = {} + + for filepath in sorted(repo_path.rglob("*")): + if not filepath.is_file(): + continue + if ".git" in filepath.parts: + continue + rel = str(filepath.relative_to(repo_path)) + try: + h = hash_file(filepath) + except OSError as exc: + logger.warning("Skipping unreadable file %s: %s", rel, exc) + continue + file_hashes[rel] = h + tree.add_leaf(rel, h) + + merkle_root = tree.build() + logger.info( + "Merkle root for %s: %s (%d files)", repo_path.name, merkle_root, len(file_hashes) + ) + + return { + "scan": scan_results, + "file_hashes": file_hashes, + "merkle_root": merkle_root, + "tree": tree, + } + + # ------------------------------------------------------------------ + # Convenience: clone + analyse in one call + # ------------------------------------------------------------------ + + def diagnose( + self, + repo_url: str, + depth: int = 1, + ref: Optional[str] = None, + ) -> Dict: + """Clone *repo_url* and return :meth:`analyze` results. + + Parameters + ---------- + repo_url: + Public repository URL. + depth: + Shallow-clone depth (0 = full clone). + ref: + Branch or tag name (optional). + + Returns + ------- + Same dict as :meth:`analyze`, with an additional ``"repo_path"`` key. + """ + repo_path = self.clone_repo(repo_url, depth=depth, ref=ref) + result = self.analyze(repo_path) + result["repo_path"] = str(repo_path) + return result From 02b0e1fb26a00910c8c8e40702b72b0cfe5ac3f9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 4 Apr 2026 08:45:08 +0000 Subject: [PATCH 2/2] fix: use `or 0` for legacy cmd handlers that return None Agent-Logs-Url: https://github.com/aidoruao/orthogonal-engineering/sessions/7b62668d-02d4-42be-be6a-4c991db7fb75 Co-authored-by: aidoruao <174227749+aidoruao@users.noreply.github.com> --- cli.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cli.py b/cli.py index 788406f7..21b76041 100644 --- a/cli.py +++ b/cli.py @@ -548,13 +548,13 @@ def main(): try: if args.command == "hash": - return cmd_hash(args) + return cmd_hash(args) or 0 elif args.command == "process": - return cmd_process(args) + return cmd_process(args) or 0 elif args.command == "backup": - return cmd_backup(args) + return cmd_backup(args) or 0 elif args.command == "manifest": - return cmd_manifest(args) + return cmd_manifest(args) or 0 elif args.command == 'index': return cmd_index(args) elif args.command == 'merkle':