Skip to content

Commit a6eaef3

Browse files
authored
Merge branch 'main' into codex/issue-36-timeout-image
2 parents 29a76ad + 163612d commit a6eaef3

10 files changed

Lines changed: 91 additions & 31 deletions

File tree

CHANGELOG.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,30 @@ All notable changes to this project are documented here.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [0.6.0] — 2026-07-12
9+
10+
### Added
11+
- `--dry-run` flag on `run`: stop right after the ingest/planning stage with
12+
the feasibility verdict and blockers, before any agent time is spent; the
13+
remaining stages are marked skipped with reason "dry-run stop" (#29, #30,
14+
thanks @Garifallos). Promoted to a first-class `dry_run` config field, so
15+
it can also be set in `readme2demo.toml`.
16+
17+
### Fixed
18+
- `report --json` crashed with an AttributeError on any run whose manifest
19+
had recorded stages (the stages dict was iterated as a list of objects),
20+
and reported `cost` as 0.0 and `commit` as null from nonexistent field
21+
names — a regression that slipped in alongside the dry-run CLI change
22+
(the old test only covered an empty manifest). Restored the real manifest
23+
fields (`total_cost_usd`, `commit_sha`) with a regression test that uses a
24+
populated manifest.
25+
26+
### Changed
27+
- CI: bump `actions/checkout` 4 → 7 and `actions/deploy-pages` 4 → 5
28+
(Dependabot).
29+
- Internal go-to-market notes (`launch/`, `RELEASE_PLAN.md`,
30+
`IMPLEMENTATION_PLAN.md`) removed from the tracked repo.
31+
832
## [0.5.0] — 2026-07-10
933

1034
### Added

CITATION.cff

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ authors:
1414
repository-code: "https://github.com/alphacrack/readme2demo"
1515
url: "https://github.com/alphacrack/readme2demo"
1616
license: MIT
17-
version: 0.4.1
18-
date-released: "2026-07-09"
17+
version: 0.6.0
18+
date-released: "2026-07-12"
1919
keywords:
2020
- ai-agents
2121
- developer-tools

docs/usage.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ provide both. At least one is required.
3434
| `--gemini [model]` | Run the whole session on Google Gemini via `GEMINI_API_KEY` (OpenHands agent + Gemini passes). Bare flag reads `--model` / `GEMINI_MODEL`. |
3535
| `--anthropic [model]` | Run the sandboxed agent on OpenHands with a Claude model via `ANTHROPIC_API_KEY`. |
3636
| `--budget-usd` | Abort the run if the agent's cost exceeds this. |
37+
| `--dry-run` | Stop after ingest/planning with the feasibility verdict and blockers — no agent time spent. |
3738
| `--skip-video` / `--with-video` | Skip or force VHS rendering. |
3839
| `--allow-docker-socket` | Mount the host Docker socket into the sandbox. Security tradeoff — trusted repos only. |
3940

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "readme2demo"
3-
version = "0.5.0"
3+
version = "0.6.0"
44
description = "Verified tutorials and demo videos from your README. An AI agent runs it in a hardened Docker sandbox and replays it in a fresh container before anything is published."
55
readme = "README.md"
66
requires-python = ">=3.10"

readme2demo.toml.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ pids_limit = 512
2828
allow_docker_socket = false
2929

3030
# --- Stages -----------------------------------------------------------------
31+
dry_run = false # stop after ingest/planning (feasibility verdict only)
3132
verify_timeout_s = 900
3233
verify_retries = 1
3334
distill_retries = 1

src/readme2demo/cli.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -335,10 +335,10 @@ def run(
335335
"else the config default.",
336336
),
337337
config_file: Optional[Path] = typer.Option(None, "--config", help="readme2demo.toml path"),
338-
# 1. ΠΡΟΣΘΗΚΗ ΕΔΩ: Δηλώνουμε το option στο Typer
339338
dry_run: bool = typer.Option(
340339
False, "--dry-run",
341-
help="Run ingest/plan only and print feasibility/blockers, then stop.",
340+
help="Run ingest/plan only and print feasibility/blockers, then stop "
341+
"— a cheap feasibility check before spending agent time.",
342342
),
343343
) -> None:
344344
"""Run the full pipeline against a repository, a step-by-step guide, or both."""
@@ -354,11 +354,8 @@ def run(
354354
config_file, engine, model, output_dir, timeout,
355355
budget_usd, max_turns, skip_video, base_image, llm_backend,
356356
)
357-
358-
# 2. ΕΔΩ (όπως το είχες): Ενημερώνουμε το config
359357
if dry_run:
360358
cfg = cfg.model_copy(update={"dry_run": True})
361-
362359
if step_by_step is not None:
363360
cfg = cfg.model_copy(update={"step_by_step": step_by_step})
364361
if repo_url is None:
@@ -448,16 +445,19 @@ def resume(
448445
@app.command()
449446
def report(
450447
run_dir: Path = typer.Argument(..., help="Path to a runs/<run-id> directory"),
451-
json_output: bool = typer.Option(False, "--json", help="Emit summary as JSON"),
448+
json_output: bool = typer.Option(False, "--json", help="Emit summary as JSON"),
452449
) -> None:
453450
"""Print a summary of a run: stage statuses, verification, cost."""
454451
manifest = Manifest.load(run_dir)
455452
if json_output:
456453
output_data = {
457-
"stages": [{"name": s.name, "status": s.status} for s in getattr(manifest, "stages", [])] if hasattr(manifest, "stages") else {},
458-
"verified": getattr(manifest, "verified", False),
459-
"cost": getattr(manifest, "cost", 0.0),
460-
"commit": getattr(manifest, "commit", None) or getattr(manifest, "repo_commit", None),
454+
"stages": [
455+
{"name": name, "status": rec.status}
456+
for name, rec in manifest.stages.items()
457+
],
458+
"verified": manifest.verified,
459+
"cost": manifest.total_cost_usd,
460+
"commit": manifest.commit_sha,
461461
}
462462
print(json.dumps(output_data, indent=2))
463463
raise typer.Exit(0)

src/readme2demo/config.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ class Config(BaseModel):
3939
pids_limit: int = 512
4040

4141
# Stages
42+
# --dry-run: stop after ingest/planning (feasibility verdict + blockers),
43+
# skipping the paid agent stage and everything downstream.
44+
dry_run: bool = False
4245
verify_timeout_s: int = 900
4346
verify_retries: int = 1 # plain script retries before distiller feedback loop
4447
distill_retries: int = 1 # distiller feedback loops on verify failure

src/readme2demo/orchestrator.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -333,10 +333,13 @@ def run(self) -> Manifest:
333333
self.manifest.stage_fail(stage, f"{type(e).__name__}: {e}")
334334
raise
335335

336-
# Έλεγχος για dry-run αμέσως μετά την επιτυχrunning του ingest στάδιου
337-
if stage == "ingest" and getattr(self.cfg, "dry_run", False):
338-
console.print("\n[bold yellow]ℹ Dry-run mode active. Stopping after ingest and planning.[/]")
339-
# Μαρκάρουμε τα επόμενα στάδια ως skipped με σαφή αιτιολογία
336+
# --dry-run: the feasibility verdict and blockers are known once
337+
# ingest completes; stop before any agent cost is spent and mark
338+
# the remaining stages skipped with a clear reason.
339+
if stage == "ingest" and self.cfg.dry_run:
340+
console.print(
341+
"\n[bold yellow]ℹ Dry run: stopping after ingest/planning.[/]"
342+
)
340343
for s in ("agent", "normalize", "distill", "verify", "render", "tutorial"):
341344
if self.manifest.stages[s].status != "completed":
342345
self.manifest.stage_skip(s, reason="dry-run stop")

tests/test_cli.py

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -386,27 +386,35 @@ def test_regression_report_keeps_bracketed_error_text(tmp_path):
386386
assert result.exit_code == 0
387387
assert "readme2demo[openai]" in result.output
388388

389-
def test_report_json_output(tmp_path, monkeypatch):
389+
def test_regression_report_json_with_recorded_stages(tmp_path):
390+
"""Regression: `report --json` crashed with AttributeError on any manifest
391+
that had recorded stages (#29 iterated the stages dict as a list of
392+
objects), and read nonexistent `cost`/`commit` fields so cost was always
393+
0.0 and commit always null. Must emit one entry per stage plus the real
394+
total_cost_usd / commit_sha values. (The old test used empty stages, so
395+
CI stayed green through the breakage.)
396+
"""
390397
import json
391398

392-
393399
manifest_data = {
394400
"run_id": "test-run-123",
395401
"verified": True,
396-
"cost": 1.50,
397-
"repo_commit": "abcdef123456",
398-
"stages": {}
402+
"total_cost_usd": 1.5,
403+
"commit_sha": "abcdef123456",
404+
"stages": {
405+
"ingest": {"status": "completed"},
406+
"agent": {"status": "failed"},
407+
},
399408
}
400-
manifest_file = tmp_path / "manifest.json"
401-
manifest_file.write_text(json.dumps(manifest_data))
409+
(tmp_path / "manifest.json").write_text(json.dumps(manifest_data))
402410

403411
result = runner.invoke(app, ["report", str(tmp_path), "--json"])
404-
412+
405413
assert result.exit_code == 0
406-
407-
parsed_output = json.loads(result.output)
408-
409-
assert parsed_output["verified"] is True
410-
assert parsed_output["cost"] == 0.0
411-
assert "commit" in parsed_output
414+
parsed = json.loads(result.output)
415+
assert parsed["verified"] is True
416+
assert parsed["cost"] == 1.5
417+
assert parsed["commit"] == "abcdef123456"
418+
assert {"name": "ingest", "status": "completed"} in parsed["stages"]
419+
assert {"name": "agent", "status": "failed"} in parsed["stages"]
412420

tests/test_orchestrator.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,3 +167,23 @@ def test_reset_from_after_verify_keeps_verdict(tmp_path: Path):
167167
assert m.verified is True
168168
m.reset_from("verify")
169169
assert m.verified is False
170+
171+
172+
def test_dry_run_stops_after_ingest(tmp_path: Path, monkeypatch):
173+
# --dry-run: feasibility verdict lands in the manifest, every later stage
174+
# is skipped with a clear reason, and no agent stage ever starts.
175+
cfg = Config(runs_dir=tmp_path, dry_run=True)
176+
orch = Orchestrator.new_run("https://github.com/x/y", cfg)
177+
178+
def fake_ingest(repo_url, run_dir, model, **kwargs):
179+
plan = make_plan(feasible=True)
180+
(run_dir / "plan.json").write_text(plan.model_dump_json())
181+
return plan, "abc1234", 0.005
182+
183+
monkeypatch.setattr(ingest_mod, "ingest", fake_ingest)
184+
manifest = orch.run()
185+
assert manifest.stages["ingest"].status == "completed"
186+
for s in ("agent", "normalize", "distill", "verify", "render", "tutorial"):
187+
assert manifest.stages[s].status == "skipped"
188+
assert manifest.stages[s].meta.get("reason") == "dry-run stop"
189+
assert manifest.verified is False

0 commit comments

Comments
 (0)