From efe0c0174b989aec11ec9f2d0c0242917a971d11 Mon Sep 17 00:00:00 2001 From: David Condrey Date: Mon, 27 Jul 2026 14:14:33 -0700 Subject: [PATCH 1/5] feat: zero-friction natural-language entry point + Claude spec-handoff MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four changes that eliminate the devplan-first requirement: 1. cli.py: if `project_path` in `build` isn't a directory, treat it as the start of the goal and use `.` — `misterdev build "add caching"` now works. 2. nl_cli.py: fast-path skips the LLM for obvious build requests (first word not a management or query word), saving a round-trip for the common case. Query-word requests still fall through to the LLM for correct routing. 3. agent.py + mcp_server.py: `build()` accepts `spec_text` — when a caller (e.g. Claude via MCP) has already written the implementation spec, misterdev skips analysis+spec-generation and goes straight to decompose→execute→verify. This is the "Claude on steroids" handoff: Claude thinks, misterdev executes. 4. registry.py: auto-writes a minimal `project.yaml` when registering a directory that lacks one, so the project persists across restarts instead of being pruned from the registry on next launch. --- misterdev/agent.py | 13 +++++-- misterdev/cli.py | 15 ++++++-- misterdev/core/execution/registry.py | 8 +++++ misterdev/mcp_server.py | 23 +++++++++++- misterdev/nl_cli.py | 54 ++++++++++++++++++++++------ tests/test_mcp_server.py | 2 +- tests/test_nl_cli.py | 30 +++++++++++++++- 7 files changed, 127 insertions(+), 18 deletions(-) diff --git a/misterdev/agent.py b/misterdev/agent.py index d693ae2..38999d8 100644 --- a/misterdev/agent.py +++ b/misterdev/agent.py @@ -651,6 +651,7 @@ def build( args: str = "", reference_dir: str | None = None, progress_cb: Optional[Callable[..., None]] = None, + spec_text: str = "", ) -> str: project = self._get_or_register(project_path) if not project: @@ -738,6 +739,7 @@ def build( report, reference_digest=reference_digest, progress_cb=progress_cb, + spec_text=spec_text, ) except BudgetExceededError as e: return self._halt_on_budget(project, report, e) @@ -1246,6 +1248,7 @@ def _run_pipeline( confirm_plan: bool = False, reference_digest: str = "", progress_cb: Optional[Callable[..., None]] = None, + spec_text: str = "", ) -> str: """Phases 1.5-6: probes, spec, decompose, (confirm), execute, validate. @@ -1323,9 +1326,13 @@ def _run_pipeline( # conservative: only claims the verifier refutes with evidence are dropped. self._verify_completeness_claims(project, assessment, report) - # Phase 2: Generate Spec - spec = self._generate_spec( - mode, prompt, assessment, project, facts=verified_facts + # Phase 2: Generate Spec (skip when caller supplies one directly) + spec = ( + spec_text + if spec_text + else self._generate_spec( + mode, prompt, assessment, project, facts=verified_facts + ) ) # Prepend the reference-implementation digest (when porting from one) so diff --git a/misterdev/cli.py b/misterdev/cli.py index 7bb82fe..6521eb1 100644 --- a/misterdev/cli.py +++ b/misterdev/cli.py @@ -392,7 +392,18 @@ def main(): proceed=args.proceed, ) elif args.command == "build": - build_args = list(args.prompt) + from pathlib import Path as _Path + + project_path = args.project_path + prompt_words = list(args.prompt) + # If project_path doesn't exist as a directory the user wrote something + # like `misterdev build "add caching"` — treat the value as the first + # word(s) of the goal and default the path to the current directory. + if project_path != "." and not _Path(project_path).is_dir(): + prompt_words = [project_path] + prompt_words + project_path = "." + + build_args = prompt_words if args.budget != 100.0: build_args.extend(["--budget", str(args.budget)]) if args.commit: @@ -417,7 +428,7 @@ def main(): build_args.extend(["--max-tasks", str(args.max_tasks)]) report = orchestrator.build( - args.project_path, " ".join(build_args), reference_dir=args.reference + project_path, " ".join(build_args), reference_dir=args.reference ) console.print("\n") if orchestrator.last_build_succeeded: diff --git a/misterdev/core/execution/registry.py b/misterdev/core/execution/registry.py index 18e234d..ffd1fbf 100644 --- a/misterdev/core/execution/registry.py +++ b/misterdev/core/execution/registry.py @@ -89,6 +89,14 @@ def register_project(self, project_path: str | Path, save: bool = True) -> Proje logger.debug(f"Project already registered: {project_id}") return self.projects[project_id] + yaml_path = path / "project.yaml" + if not yaml_path.exists(): + try: + yaml_path.write_text(f"name: {path.name}\n", encoding="utf-8") + logger.info(f"Created minimal project.yaml at {yaml_path}") + except OSError as e: + logger.warning(f"Could not write project.yaml at {yaml_path}: {e}") + config = self.config_manager.load_project_config(path) project = Project(path, config) self.projects[project_id] = project diff --git a/misterdev/mcp_server.py b/misterdev/mcp_server.py index da3effc..0889a61 100644 --- a/misterdev/mcp_server.py +++ b/misterdev/mcp_server.py @@ -272,6 +272,19 @@ def build( examples=["/Users/me/code/donor-impl"], ), ] = None, + spec_text: Annotated[ + Optional[str], + Field( + description=( + "A complete implementation spec in markdown — supply this when you " + "have already analysed the codebase and written the spec yourself " + "(e.g. from a Claude conversation). misterdev will skip its own " + "analysis and spec-generation phases and go straight to " + "decompose → execute → verify using your spec. Omit to let " + "misterdev analyse and generate the spec from ``goal``." + ), + ), + ] = None, ) -> str: """Autonomously plan AND execute a goal in a project, from scratch. @@ -283,6 +296,9 @@ def build( Related: ``run`` (execute an existing plan), ``status`` (inspect tasks). Pass ``reference_dir`` to port from an existing implementation: its module/symbol map is extracted read-only and guides the plan. + Pass ``spec_text`` when you have already written the implementation spec + (e.g. in a Claude conversation) — misterdev skips its own planning phase + and executes your spec directly, making it Claude's execution backend. DESTRUCTIVE side effects: edits files and makes git commits, and calls an external LLM provider (open-world, non-idempotent). It refuses to run on a @@ -299,7 +315,12 @@ def build( parts.append("--parallel") if max_tasks is not None: parts += ["--max-tasks", str(max_tasks)] - report = orch.build(path, " ".join(parts), reference_dir=reference_dir) + report = orch.build( + path, + " ".join(parts), + reference_dir=reference_dir, + spec_text=spec_text or "", + ) outcome = "succeeded" if orch.last_build_succeeded else "did not fully succeed" return f"Build {outcome}.\n\n{report}" diff --git a/misterdev/nl_cli.py b/misterdev/nl_cli.py index d9a13b3..17d0122 100644 --- a/misterdev/nl_cli.py +++ b/misterdev/nl_cli.py @@ -117,22 +117,56 @@ def _dispatch(intent: Dict[str, Any], orchestrator) -> int: return 1 +_MANAGEMENT_WORDS = {"scan", "list", "status", "report", "run", "plan"} +_QUERY_WORDS = { + "what", + "how", + "why", + "show", + "get", + "find", + "check", + "is", + "are", + "does", + "do", + "did", + "has", + "have", + "which", + "where", + "when", + "who", +} + + +def _fast_route(request: str) -> Dict[str, Any] | None: + """Return a build intent without an LLM call when the request clearly + describes coding work (first word is not a management or query word).""" + first = request.strip().split()[0].lower() if request.strip() else "" + if first and first not in _MANAGEMENT_WORDS and first not in _QUERY_WORDS: + return {"command": "build", "path": ".", "goal": request.strip()} + return None + + def route(request: str, orchestrator, confirm=input) -> int: """Resolve a plain-English request to an action and run it. Returns a process exit code. ``confirm`` is injectable for testing. """ - cfg = ConfigManager().load_project_config(".") - try: - client = create_llm_client(cfg) - except Exception as e: - console.print( - "[yellow]Natural-language mode needs an LLM configured " - f"(model + API key). {e}[/]\nRun `misterdev --help` for the flag-based CLI." - ) - return 1 + intent = _fast_route(request) + if intent is None: + cfg = ConfigManager().load_project_config(".") + try: + client = create_llm_client(cfg) + except Exception as e: + console.print( + "[yellow]Natural-language mode needs an LLM configured " + f"(model + API key). {e}[/]\nRun `misterdev --help` for the flag-based CLI." + ) + return 1 + intent = parse_intent(request, client) - intent = parse_intent(request, client) cmd = intent.get("command") if cmd not in KNOWN_COMMANDS: console.print( diff --git a/tests/test_mcp_server.py b/tests/test_mcp_server.py index 91913fd..c1368ae 100644 --- a/tests/test_mcp_server.py +++ b/tests/test_mcp_server.py @@ -11,7 +11,7 @@ class _FakeOrch: last_build_succeeded = True calls: dict = {} - def build(self, path, args, reference_dir=None, progress_cb=None): + def build(self, path, args, reference_dir=None, progress_cb=None, spec_text=""): _FakeOrch.calls["build"] = (path, args, reference_dir) if progress_cb is not None: progress_cb(done=1, total=1, phase="building") diff --git a/tests/test_nl_cli.py b/tests/test_nl_cli.py index 5f74af7..e48b1ca 100644 --- a/tests/test_nl_cli.py +++ b/tests/test_nl_cli.py @@ -98,5 +98,33 @@ def _no_confirm(_p): def test_route_unmappable_request(monkeypatch): _stub_client(monkeypatch, '{"command": "nonsense"}') - rc = nl_cli.route("blah blah", _FakeOrch(), confirm=lambda _p: "y") + # "what" is a query word so it falls through to the LLM, which returns an + # unknown command — the router should return 1. + rc = nl_cli.route("what is blah blah", _FakeOrch(), confirm=lambda _p: "y") assert rc == 1 + + +def test_fast_route_skips_llm_for_build_requests(monkeypatch): + called = [] + monkeypatch.setattr( + nl_cli, "parse_intent", lambda req, client: called.append(1) or {} + ) + orch = _FakeOrch() + _FakeOrch.calls = {} + nl_cli.route("add caching to the API", orch, confirm=lambda _p: "y") + assert not called, "LLM should not be called for an obvious build request" + assert "build" in _FakeOrch.calls + + +def test_fast_route_falls_back_to_llm_for_queries(monkeypatch): + called = [] + _stub_client(monkeypatch, '{"command": "status", "path": "."}') + orig = nl_cli.parse_intent + + def spy(req, client): + called.append(req) + return orig(req, client) + + monkeypatch.setattr(nl_cli, "parse_intent", spy) + nl_cli.route("what is the current status", _FakeOrch(), confirm=lambda _p: "n") + assert called, "LLM should be called for a query-word request" From fad2862f55b151c2845c84bee548647cb859774c Mon Sep 17 00:00:00 2001 From: David Condrey Date: Mon, 27 Jul 2026 14:58:36 -0700 Subject: [PATCH 2/5] feat: eliminate confirm friction + fix run routing + rich build output MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - nl_cli: fast-routed requests skip the "proceed? [Y/n]" prompt — user was already explicit; confirm only fires when the LLM had to disambiguate - cli: `misterdev run "add feature X"` redirects to build when project_path isn't a real directory, matching the fix already applied to build - nl_cli: build result rendered in Rich Panel with pass/fail colour, matching the build subcommand's output instead of printing raw text --- misterdev/cli.py | 20 ++++++++++++++------ misterdev/nl_cli.py | 16 +++++++++++++--- tests/test_nl_cli.py | 4 +++- 3 files changed, 30 insertions(+), 10 deletions(-) diff --git a/misterdev/cli.py b/misterdev/cli.py index 6521eb1..5c83e59 100644 --- a/misterdev/cli.py +++ b/misterdev/cli.py @@ -374,15 +374,23 @@ def main(): _print_doctor(result) sys.exit(result.get("exit_code", 1)) elif args.command == "run": + from pathlib import Path as _Path + + run_path = args.project_path + # If project_path isn't a directory the user likely typed a goal + # (e.g. `misterdev run "add feature X"`); route to build instead. + if run_path != "." and not _Path(run_path).is_dir() and not args.task: + from misterdev.nl_cli import route as _nl_route + + sys.exit(_nl_route(run_path, orchestrator)) + if args.task: - logger.info( - f"Running specific task {args.task} in project {args.project_path}" - ) - orchestrator.run_task(args.project_path, args.task) + logger.info(f"Running specific task {args.task} in project {run_path}") + orchestrator.run_task(run_path, args.task) else: - logger.info(f"Running pending tasks for project {args.project_path}") + logger.info(f"Running pending tasks for project {run_path}") orchestrator.run_project( - args.project_path, + run_path, dry_run=args.dry_run, skip_preflight=args.skip_preflight, force=args.force, diff --git a/misterdev/nl_cli.py b/misterdev/nl_cli.py index 17d0122..fa0abfe 100644 --- a/misterdev/nl_cli.py +++ b/misterdev/nl_cli.py @@ -89,12 +89,21 @@ def preview(intent: Dict[str, Any]) -> str: def _dispatch(intent: Dict[str, Any], orchestrator) -> int: + from rich.markdown import Markdown + from rich.panel import Panel + cmd = intent.get("command") path = intent.get("path") or "." if cmd == "build": report = orchestrator.build(path, _build_args(intent)) - console.print(report) - return 0 if orchestrator.last_build_succeeded else 1 + succeeded = orchestrator.last_build_succeeded + title = ( + "[bold green]Build Complete[/bold green]" + if succeeded + else "[bold red]Build Failed Validation[/bold red]" + ) + console.print(Panel(Markdown(report), title=title, expand=False)) + return 0 if succeeded else 1 if cmd == "run": orchestrator.run_project(path, dry_run=bool(intent.get("dry_run"))) return 0 @@ -155,6 +164,7 @@ def route(request: str, orchestrator, confirm=input) -> int: Returns a process exit code. ``confirm`` is injectable for testing. """ intent = _fast_route(request) + needs_confirm = intent is None # explicit fast-route = user was already clear if intent is None: cfg = ConfigManager().load_project_config(".") try: @@ -175,7 +185,7 @@ def route(request: str, orchestrator, confirm=input) -> int: return 1 console.print(f"[dim]→ I'll run:[/] misterdev {preview(intent)}") - if cmd in _MUTATING: + if needs_confirm and cmd in _MUTATING: answer = confirm("proceed? [Y/n] ").strip().lower() if answer and answer not in ("y", "yes"): console.print("Cancelled.") diff --git a/tests/test_nl_cli.py b/tests/test_nl_cli.py index e48b1ca..b0331a8 100644 --- a/tests/test_nl_cli.py +++ b/tests/test_nl_cli.py @@ -80,8 +80,10 @@ def test_route_confirms_then_runs_mutating(monkeypatch): def test_route_cancel_skips_execution(monkeypatch): + # Use a query-word prefix so the request falls through to the LLM and the + # confirm prompt fires; the fast-path skips confirm by design. _stub_client(monkeypatch, '{"command": "build", "path": ".", "goal": "x"}') - rc = nl_cli.route("x", _FakeOrch(), confirm=lambda _p: "n") + rc = nl_cli.route("do build x", _FakeOrch(), confirm=lambda _p: "n") assert rc == 0 assert "build" not in _FakeOrch.calls From 6c17a95ac65d159cb43eca3e9864e3e26dc9f41a Mon Sep 17 00:00:00 2001 From: David Condrey Date: Mon, 27 Jul 2026 15:06:47 -0700 Subject: [PATCH 3/5] feat: build spinner, destructive-verb safety guard, run empty-state hint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - nl_cli: show "Building…" spinner during autonomous build so users know it's running (builds can take minutes; silent=hung from user perspective) - nl_cli: fast-routed requests with destructive verbs (delete/remove/drop/ destroy/wipe/erase/purge) still prompt for confirmation — removes a footgun introduced when we cut the confirm for obvious build requests - cli: `misterdev run` with no planned tasks prints a hint to use `misterdev build [goal]` instead of silently doing nothing --- misterdev/cli.py | 7 +++++++ misterdev/nl_cli.py | 10 ++++++++-- tests/test_nl_cli.py | 12 ++++++++++++ 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/misterdev/cli.py b/misterdev/cli.py index 5c83e59..581e1bc 100644 --- a/misterdev/cli.py +++ b/misterdev/cli.py @@ -388,6 +388,8 @@ def main(): logger.info(f"Running specific task {args.task} in project {run_path}") orchestrator.run_task(run_path, args.task) else: + status_before = orchestrator.get_project_status(run_path) + has_tasks = bool(status_before.get("tasks")) logger.info(f"Running pending tasks for project {run_path}") orchestrator.run_project( run_path, @@ -399,6 +401,11 @@ def main(): budget=args.budget, proceed=args.proceed, ) + if not has_tasks and not args.tasks: + console.print( + "[dim]No planned tasks found.[/] " + "Use [bold]misterdev build [goal][/bold] to autonomously plan and execute work." + ) elif args.command == "build": from pathlib import Path as _Path diff --git a/misterdev/nl_cli.py b/misterdev/nl_cli.py index fa0abfe..7e8278b 100644 --- a/misterdev/nl_cli.py +++ b/misterdev/nl_cli.py @@ -89,13 +89,16 @@ def preview(intent: Dict[str, Any]) -> str: def _dispatch(intent: Dict[str, Any], orchestrator) -> int: + from rich.live import Live from rich.markdown import Markdown from rich.panel import Panel + from rich.text import Text cmd = intent.get("command") path = intent.get("path") or "." if cmd == "build": - report = orchestrator.build(path, _build_args(intent)) + with Live(Text("Building…", style="dim"), console=console, transient=True): + report = orchestrator.build(path, _build_args(intent)) succeeded = orchestrator.last_build_succeeded title = ( "[bold green]Build Complete[/bold green]" @@ -147,6 +150,7 @@ def _dispatch(intent: Dict[str, Any], orchestrator) -> int: "when", "who", } +_DESTRUCTIVE_VERBS = {"delete", "remove", "drop", "destroy", "wipe", "erase", "purge"} def _fast_route(request: str) -> Dict[str, Any] | None: @@ -164,7 +168,9 @@ def route(request: str, orchestrator, confirm=input) -> int: Returns a process exit code. ``confirm`` is injectable for testing. """ intent = _fast_route(request) - needs_confirm = intent is None # explicit fast-route = user was already clear + first_word = request.strip().split()[0].lower() if request.strip() else "" + # Skip confirm for fast-routed requests unless the verb is destructive. + needs_confirm = intent is None or first_word in _DESTRUCTIVE_VERBS if intent is None: cfg = ConfigManager().load_project_config(".") try: diff --git a/tests/test_nl_cli.py b/tests/test_nl_cli.py index b0331a8..19a57e0 100644 --- a/tests/test_nl_cli.py +++ b/tests/test_nl_cli.py @@ -130,3 +130,15 @@ def spy(req, client): monkeypatch.setattr(nl_cli, "parse_intent", spy) nl_cli.route("what is the current status", _FakeOrch(), confirm=lambda _p: "n") assert called, "LLM should be called for a query-word request" + + +def test_destructive_verb_still_confirms(monkeypatch): + monkeypatch.setattr(nl_cli, "parse_intent", lambda req, client: {}) + confirmed = [] + orch = _FakeOrch() + _FakeOrch.calls = {} + nl_cli.route( + "delete the auth module", orch, confirm=lambda _p: confirmed.append(1) or "n" + ) + assert confirmed, "destructive verb must prompt for confirmation even on fast path" + assert "build" not in _FakeOrch.calls From 0d5ed6497829eacddd4f7ebeaa2130b789cd60e3 Mon Sep 17 00:00:00 2001 From: David Condrey Date: Mon, 27 Jul 2026 15:10:33 -0700 Subject: [PATCH 4/5] feat: animated progress spinner with live task updates; suppress noisy preview MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - nl_cli: replace static Text with animated rich.Spinner and pass progress_cb to build() so the spinner text updates as tasks complete ("wave 2 [3/7]") - nl_cli: suppress the "→ I'll run:" preview line for fast-routed requests — user was already explicit; the echo adds noise with no disambiguation value (still shown when LLM had to interpret an ambiguous query) --- misterdev/nl_cli.py | 16 ++++++++++++---- tests/test_nl_cli.py | 2 +- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/misterdev/nl_cli.py b/misterdev/nl_cli.py index 7e8278b..2aec09c 100644 --- a/misterdev/nl_cli.py +++ b/misterdev/nl_cli.py @@ -92,13 +92,20 @@ def _dispatch(intent: Dict[str, Any], orchestrator) -> int: from rich.live import Live from rich.markdown import Markdown from rich.panel import Panel - from rich.text import Text + from rich.spinner import Spinner cmd = intent.get("command") path = intent.get("path") or "." if cmd == "build": - with Live(Text("Building…", style="dim"), console=console, transient=True): - report = orchestrator.build(path, _build_args(intent)) + spinner = Spinner("dots", text="Building…") + + def _on_progress(done, total, phase): + spinner.text = f"{phase} [{done}/{total}]" if total else phase + + with Live(spinner, console=console, transient=True, refresh_per_second=10): + report = orchestrator.build( + path, _build_args(intent), progress_cb=_on_progress + ) succeeded = orchestrator.last_build_succeeded title = ( "[bold green]Build Complete[/bold green]" @@ -190,7 +197,8 @@ def route(request: str, orchestrator, confirm=input) -> int: ) return 1 - console.print(f"[dim]→ I'll run:[/] misterdev {preview(intent)}") + if needs_confirm: + console.print(f"[dim]→ I'll run:[/] misterdev {preview(intent)}") if needs_confirm and cmd in _MUTATING: answer = confirm("proceed? [Y/n] ").strip().lower() if answer and answer not in ("y", "yes"): diff --git a/tests/test_nl_cli.py b/tests/test_nl_cli.py index 19a57e0..d8b1913 100644 --- a/tests/test_nl_cli.py +++ b/tests/test_nl_cli.py @@ -15,7 +15,7 @@ class _FakeOrch: last_build_succeeded = True calls: dict = {} - def build(self, path, args): + def build(self, path, args, progress_cb=None, **kwargs): _FakeOrch.calls["build"] = (path, args) return "REPORT" From c03d0a83f2bd4851beb609037ac5fef77a05e2d6 Mon Sep 17 00:00:00 2001 From: David Condrey Date: Mon, 27 Jul 2026 15:15:18 -0700 Subject: [PATCH 5/5] feat: skip analysis when spec_text provided; spinner on CLI build command MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - agent.py: when spec_text is supplied (Claude handoff path), substitute a zero-cost ProjectAssessment() stub for _analyze() — saves the full analysis phase (LLM calls + build/test runs) when Claude already has the context; health-baseline warnings also skipped (no health data) - cli.py: add animated Rich spinner + progress_cb to the `build` subcommand so `misterdev build "goal"` shows live task progress, matching the UX of the natural-language path added in the previous commits --- misterdev/agent.py | 16 +++++++++++----- misterdev/cli.py | 18 +++++++++++++++--- 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/misterdev/agent.py b/misterdev/agent.py index 38999d8..e7c2ccb 100644 --- a/misterdev/agent.py +++ b/misterdev/agent.py @@ -720,14 +720,20 @@ def build( # exhausted by pre-execution analysis+probes+spec and crashed the run.) report = None try: - # Phase 1: Analysis - assessment = self._analyze(project, env_activate) + # Phase 1: Analysis — skipped when the caller supplies a spec directly + # (spec_text path: Claude already analysed the codebase; use a zero-cost + # stub so decomposition still has a well-typed object to read). + if spec_text: + assessment = ProjectAssessment() + else: + assessment = self._analyze(project, env_activate) report = BuildReport(mode, project.name, assessment, start_time) report.health_before = assessment.health.model_copy() - _warn_if_baseline_broken(assessment, report) - _warn_if_no_test_gate(assessment, project, report) - _warn_if_test_gate_is_noop(assessment, report) + if not spec_text: + _warn_if_baseline_broken(assessment, report) + _warn_if_no_test_gate(assessment, project, report) + _warn_if_test_gate_is_noop(assessment, report) return self._run_pipeline( project, diff --git a/misterdev/cli.py b/misterdev/cli.py index 581e1bc..06f871f 100644 --- a/misterdev/cli.py +++ b/misterdev/cli.py @@ -442,9 +442,21 @@ def main(): if args.max_tasks is not None: build_args.extend(["--max-tasks", str(args.max_tasks)]) - report = orchestrator.build( - project_path, " ".join(build_args), reference_dir=args.reference - ) + from rich.live import Live + from rich.spinner import Spinner + + _spinner = Spinner("dots", text="Analyzing…") + + def _on_progress(done, total, phase): + _spinner.text = f"{phase} [{done}/{total}]" if total else phase + + with Live(_spinner, console=console, transient=True, refresh_per_second=10): + report = orchestrator.build( + project_path, + " ".join(build_args), + reference_dir=args.reference, + progress_cb=_on_progress, + ) console.print("\n") if orchestrator.last_build_succeeded: console.print(