claude-code-generator 0.2.1__tar.gz → 0.2.3__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.
Files changed (115) hide show
  1. {claude_code_generator-0.2.1/src/claude_code_generator.egg-info → claude_code_generator-0.2.3}/PKG-INFO +1 -1
  2. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/pyproject.toml +1 -1
  3. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3/src/claude_code_generator.egg-info}/PKG-INFO +1 -1
  4. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/src/code_generator/__init__.py +1 -1
  5. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/src/code_generator/commands/_dispatch.py +58 -0
  6. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/src/code_generator/orchestrator/phase2_review.py +13 -0
  7. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/src/code_generator/state.py +2 -0
  8. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/tests/test_generate_resume.py +126 -0
  9. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/tests/test_phase2.py +74 -0
  10. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/LICENSE +0 -0
  11. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/README.md +0 -0
  12. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/setup.cfg +0 -0
  13. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/src/claude_code_generator.egg-info/SOURCES.txt +0 -0
  14. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/src/claude_code_generator.egg-info/dependency_links.txt +0 -0
  15. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/src/claude_code_generator.egg-info/entry_points.txt +0 -0
  16. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/src/claude_code_generator.egg-info/requires.txt +0 -0
  17. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/src/claude_code_generator.egg-info/top_level.txt +0 -0
  18. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/src/code_generator/agents.py +0 -0
  19. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/src/code_generator/cli.py +0 -0
  20. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/src/code_generator/commands/__init__.py +0 -0
  21. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/src/code_generator/commands/_detect.py +0 -0
  22. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/src/code_generator/commands/_resume.py +0 -0
  23. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/src/code_generator/commands/generate.py +0 -0
  24. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/src/code_generator/commands/init.py +0 -0
  25. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/src/code_generator/commands/optimize.py +0 -0
  26. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/src/code_generator/commands/review.py +0 -0
  27. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/src/code_generator/commands/status.py +0 -0
  28. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/src/code_generator/effort.py +0 -0
  29. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/src/code_generator/env.py +0 -0
  30. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/src/code_generator/gh/__init__.py +0 -0
  31. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/src/code_generator/gh/core.py +0 -0
  32. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/src/code_generator/gh/issues.py +0 -0
  33. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/src/code_generator/gh/labels.py +0 -0
  34. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/src/code_generator/gh/milestones.py +0 -0
  35. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/src/code_generator/git_ops.py +0 -0
  36. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/src/code_generator/logging_setup.py +0 -0
  37. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/src/code_generator/orchestrator/__init__.py +0 -0
  38. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/src/code_generator/orchestrator/_comments.py +0 -0
  39. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/src/code_generator/orchestrator/cycle_loop.py +0 -0
  40. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/src/code_generator/orchestrator/phase0_complexity.py +0 -0
  41. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/src/code_generator/orchestrator/phase1_plan.py +0 -0
  42. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/src/code_generator/orchestrator/phase3_4_implement.py +0 -0
  43. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/src/code_generator/orchestrator/phase5_closure.py +0 -0
  44. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/src/code_generator/orchestrator/phase6_test.py +0 -0
  45. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/src/code_generator/orchestrator/phase7_commit.py +0 -0
  46. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/src/code_generator/prompts/__init__.py +0 -0
  47. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/src/code_generator/prompts/prompt-optimize-requirements.md +0 -0
  48. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/src/code_generator/prompts/prompt-phase-0-complexity.md +0 -0
  49. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/src/code_generator/prompts/prompt-phase-1-planning.md +0 -0
  50. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/src/code_generator/prompts/prompt-phase-2-issue-review.md +0 -0
  51. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/src/code_generator/prompts/prompt-phase-3-implementation.md +0 -0
  52. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/src/code_generator/prompts/prompt-phase-5-final-review.md +0 -0
  53. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/src/code_generator/prompts/prompt-phase-6-test.md +0 -0
  54. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/src/code_generator/prompts/prompt-phase-7-commit.md +0 -0
  55. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/src/code_generator/prompts/prompt-review.md +0 -0
  56. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/src/code_generator/requirements_structure.py +0 -0
  57. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/src/code_generator/runner/__init__.py +0 -0
  58. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/src/code_generator/runner/message_parsing.py +0 -0
  59. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/src/code_generator/runner/options.py +0 -0
  60. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/src/code_generator/runner/protocol.py +0 -0
  61. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/src/code_generator/runner/rate_limit.py +0 -0
  62. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/src/code_generator/runner/retry.py +0 -0
  63. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/src/code_generator/runner/sdk_runner.py +0 -0
  64. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/src/code_generator/runner/subprocess_runner.py +0 -0
  65. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/src/code_generator/runner/types.py +0 -0
  66. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/src/code_generator/runner/utils.py +0 -0
  67. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/src/code_generator/templates/__init__.py +0 -0
  68. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/src/code_generator/templates/angular.md +0 -0
  69. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/src/code_generator/templates/base.md +0 -0
  70. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/src/code_generator/templates/fastapi.md +0 -0
  71. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/src/code_generator/templates/finance.md +0 -0
  72. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/src/code_generator/templates/fullstack.md +0 -0
  73. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/src/code_generator/templates/nestjs.md +0 -0
  74. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/src/code_generator/templates/python-cli.md +0 -0
  75. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/tests/test_agents.py +0 -0
  76. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/tests/test_comments.py +0 -0
  77. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/tests/test_commit_message.py +0 -0
  78. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/tests/test_cycle_loop.py +0 -0
  79. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/tests/test_cycle_loop_multicycle.py +0 -0
  80. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/tests/test_delta_planning.py +0 -0
  81. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/tests/test_detect.py +0 -0
  82. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/tests/test_effort.py +0 -0
  83. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/tests/test_env.py +0 -0
  84. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/tests/test_generate.py +0 -0
  85. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/tests/test_gh.py +0 -0
  86. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/tests/test_gh_labels.py +0 -0
  87. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/tests/test_gh_milestones.py +0 -0
  88. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/tests/test_gh_submodules.py +0 -0
  89. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/tests/test_git_ops.py +0 -0
  90. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/tests/test_init.py +0 -0
  91. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/tests/test_logging_setup.py +0 -0
  92. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/tests/test_message_parsing.py +0 -0
  93. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/tests/test_optimize.py +0 -0
  94. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/tests/test_options.py +0 -0
  95. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/tests/test_phase0.py +0 -0
  96. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/tests/test_phase1.py +0 -0
  97. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/tests/test_phase3_4.py +0 -0
  98. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/tests/test_phase5.py +0 -0
  99. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/tests/test_phase6.py +0 -0
  100. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/tests/test_phase7.py +0 -0
  101. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/tests/test_phase_token_logging.py +0 -0
  102. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/tests/test_prompts.py +0 -0
  103. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/tests/test_rate_limit.py +0 -0
  104. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/tests/test_requirements_structure.py +0 -0
  105. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/tests/test_retry.py +0 -0
  106. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/tests/test_review.py +0 -0
  107. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/tests/test_runner_protocol.py +0 -0
  108. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/tests/test_runner_protocol_annotations.py +0 -0
  109. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/tests/test_runner_types.py +0 -0
  110. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/tests/test_runner_utils.py +0 -0
  111. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/tests/test_sdk_runner.py +0 -0
  112. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/tests/test_state.py +0 -0
  113. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/tests/test_status.py +0 -0
  114. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/tests/test_subprocess_runner.py +0 -0
  115. {claude_code_generator-0.2.1 → claude_code_generator-0.2.3}/tests/test_version.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: claude-code-generator
3
- Version: 0.2.1
3
+ Version: 0.2.3
4
4
  Summary: Orchestrator CLI that drives Claude Code end-to-end to generate whole projects from a requirements.md file.
5
5
  Author: Silvio Baratto
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "claude-code-generator"
7
- version = "0.2.1"
7
+ version = "0.2.3"
8
8
  description = "Orchestrator CLI that drives Claude Code end-to-end to generate whole projects from a requirements.md file."
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: claude-code-generator
3
- Version: 0.2.1
3
+ Version: 0.2.3
4
4
  Summary: Orchestrator CLI that drives Claude Code end-to-end to generate whole projects from a requirements.md file.
5
5
  Author: Silvio Baratto
6
6
  License: MIT
@@ -1,3 +1,3 @@
1
1
  """code-generator: orchestrator CLI for end-to-end project generation."""
2
2
 
3
- __version__ = "0.2.1"
3
+ __version__ = "0.2.3"
@@ -25,6 +25,56 @@ _logger = logging.getLogger(__name__)
25
25
  __all__ = ["dispatch_async", "dispatch_orchestrator"]
26
26
 
27
27
 
28
+ def _reset_failed_cycles_for_continue(
29
+ st: state_module.State,
30
+ *,
31
+ target_cycle: int | None,
32
+ state_path: Path,
33
+ ) -> None:
34
+ """Reset ``failed`` cycles back to ``open`` on ``--continue``.
35
+
36
+ When a cycle crashes mid-run, ``cycle_loop.run_multi_cycle`` marks it as
37
+ ``status="failed"`` and persists the exception in ``state.last_error``.
38
+ The next ``--continue`` must treat these as retryable, otherwise the user
39
+ has to hand-edit ``state.json`` every time a cycle crashes — which
40
+ defeats the whole point of ``--continue``.
41
+
42
+ When ``target_cycle`` is set (``--cycle N``), only that cycle is reset.
43
+ Otherwise every failed cycle is reset. ``state.last_error`` and
44
+ ``state.rate_limit_type`` are cleared on any successful reset so the
45
+ status line stops showing red for a resolved failure.
46
+
47
+ Args:
48
+ st: Root state (mutated in place).
49
+ target_cycle: When set, only reset this cycle id; otherwise reset all.
50
+ state_path: Path to ``state.json`` for atomic persistence.
51
+ """
52
+ from code_generator import state as _state_module
53
+
54
+ reset_ids: list[int] = []
55
+ for c in st.cycles:
56
+ if c.status != "failed":
57
+ continue
58
+ if target_cycle is not None and c.id != target_cycle:
59
+ continue
60
+ c.status = "open"
61
+ reset_ids.append(c.id)
62
+
63
+ if not reset_ids:
64
+ return
65
+
66
+ if getattr(st, "last_error", None):
67
+ st.last_error = None
68
+ if getattr(st, "rate_limit_type", None):
69
+ st.rate_limit_type = None
70
+
71
+ _state_module.save_state(state_path, st)
72
+ _logger.info(
73
+ "--continue: reset failed cycles to open: %s",
74
+ ", ".join(str(i) for i in reset_ids),
75
+ )
76
+
77
+
28
78
  async def _apply_delta_plan(
29
79
  st: state_module.State,
30
80
  project_dir: Path,
@@ -250,6 +300,14 @@ def dispatch_orchestrator(
250
300
  start_cycle: int | None = None
251
301
 
252
302
  if continue_:
303
+ # A cycle that crashed mid-run is marked "failed" by the exception
304
+ # handler in cycle_loop.run_multi_cycle. Without this reset, `--continue`
305
+ # would skip it forever because `_eligible_cycles` only returns "open"
306
+ # cycles. Semantically, `--continue` means "retry from where we
307
+ # crashed", so flip any failed cycle back to "open" and clear the
308
+ # stale error. Explicit --cycle N also benefits: reset just the target.
309
+ _reset_failed_cycles_for_continue(st, target_cycle=cycle, state_path=state_path)
310
+
253
311
  # Resume from the *next* phase/cycle after the last completed one.
254
312
  if effective_mode == "multi-cycle":
255
313
  start_cycle, start_phase = resolve_continue_multi_cycle(st)
@@ -63,6 +63,14 @@ async def run(
63
63
  if issue.status != "open":
64
64
  continue
65
65
 
66
+ # Skip issues already reviewed in a prior run. Phase 2 never closes
67
+ # the issue (only Phase 3/4 does), so status alone cannot tell us
68
+ # whether the review already ran. Without this check, --continue
69
+ # re-reviews every open issue from scratch on every resume.
70
+ if issue.reviewed:
71
+ logger.info("Phase 2: issue #%d already reviewed — skipping.", issue.number)
72
+ continue
73
+
66
74
  logger.info("Phase 2: reviewing issue #%d (agent=%s).", issue.number, issue.agent)
67
75
 
68
76
  comments_section = fetch_formatted_comments(issue.number, logger)
@@ -137,6 +145,11 @@ async def run(
137
145
  phase_total += issue_usage
138
146
 
139
147
  if reviewed:
148
+ # Persist the per-issue reviewed flag immediately so a crash (or
149
+ # --continue after a rate-limit pause) does not re-review work
150
+ # that already succeeded.
151
+ issue.reviewed = True
152
+ save_state(state_path, state)
140
153
  logger.info("Phase 2: issue #%d reviewed.", issue.number)
141
154
 
142
155
  target = cycle if cycle is not None else state
@@ -27,6 +27,7 @@ class IssueState:
27
27
  status: Literal["open", "closed"]
28
28
  agent: str
29
29
  labels: list[str] = field(default_factory=list)
30
+ reviewed: bool = False
30
31
 
31
32
 
32
33
  @dataclass
@@ -112,6 +113,7 @@ def _issue_from_dict(d: dict) -> IssueState: # type: ignore[type-arg]
112
113
  status=d["status"],
113
114
  agent=d["agent"],
114
115
  labels=d.get("labels", []),
116
+ reviewed=d.get("reviewed", False),
115
117
  )
116
118
 
117
119
 
@@ -1337,3 +1337,129 @@ class TestNoOpDetection:
1337
1337
 
1338
1338
  st_after = state_module.load_state(cg / "state.json")
1339
1339
  assert st_after.requirements_hash == new_hash
1340
+
1341
+
1342
+ # ---------------------------------------------------------------------------
1343
+ # --continue resets failed cycles back to open (retry-on-continue)
1344
+ # ---------------------------------------------------------------------------
1345
+
1346
+
1347
+ class TestContinueResetsFailedCycles:
1348
+ """Regression guard: `--continue` must retry cycles marked `status=failed`.
1349
+
1350
+ When a cycle crashes mid-run, `cycle_loop.run_multi_cycle` sets
1351
+ `cycle.status = "failed"` and persists `state.last_error`. Without the
1352
+ reset, `_eligible_cycles` filters out failed cycles and `--continue`
1353
+ prints "all eligible cycles complete" without actually running anything.
1354
+ """
1355
+
1356
+ def test_continue_resets_failed_cycle_to_open(
1357
+ self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
1358
+ ) -> None:
1359
+ """--continue flips cycles[0].status from 'failed' back to 'open' and clears last_error."""
1360
+ monkeypatch.chdir(tmp_path)
1361
+
1362
+ cg = tmp_path / ".code-generator"
1363
+ cg.mkdir(parents=True, exist_ok=True)
1364
+ st = state_module.State(
1365
+ mode="multi-cycle", # type: ignore[arg-type]
1366
+ started_at="2026-01-01T00:00:00Z",
1367
+ updated_at="2026-01-01T00:00:00Z",
1368
+ current_cycle=1,
1369
+ phase=None,
1370
+ cycles=[
1371
+ _make_cycle(cycle_id=1, phase=1, status="failed"),
1372
+ _make_cycle(cycle_id=2, phase=0, status="open"),
1373
+ ],
1374
+ )
1375
+ st.last_error = "SDK session exhausted max_turns budget"
1376
+ state_module.save_state(cg / "state.json", st)
1377
+
1378
+ captured: list[dict] = []
1379
+
1380
+ async def mock_multi(state, *_args, start_cycle=None, start_phase=1, **_kwargs):
1381
+ captured.append({"start_cycle": start_cycle, "start_phase": start_phase})
1382
+ return state
1383
+
1384
+ from code_generator.orchestrator import cycle_loop
1385
+
1386
+ monkeypatch.setattr(cycle_loop, "run_multi_cycle", mock_multi)
1387
+ _mock_phase0_single(monkeypatch)
1388
+
1389
+ result = runner.invoke(app, ["generate", "--continue"])
1390
+ assert result.exit_code == 0, result.output
1391
+
1392
+ st_after = state_module.load_state(cg / "state.json")
1393
+ assert st_after.cycles[0].status == "open"
1394
+ assert st_after.last_error is None
1395
+ assert captured == [{"start_cycle": 1, "start_phase": 2}]
1396
+
1397
+ def test_continue_resets_only_target_cycle_when_cycle_flag_set(
1398
+ self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
1399
+ ) -> None:
1400
+ """--continue --cycle 2 resets cycle 2 only, leaves cycle 1 failed."""
1401
+ monkeypatch.chdir(tmp_path)
1402
+
1403
+ cg = tmp_path / ".code-generator"
1404
+ cg.mkdir(parents=True, exist_ok=True)
1405
+ st = state_module.State(
1406
+ mode="multi-cycle", # type: ignore[arg-type]
1407
+ started_at="2026-01-01T00:00:00Z",
1408
+ updated_at="2026-01-01T00:00:00Z",
1409
+ current_cycle=2,
1410
+ phase=None,
1411
+ cycles=[
1412
+ _make_cycle(cycle_id=1, phase=3, status="failed"),
1413
+ _make_cycle(cycle_id=2, phase=4, status="failed"),
1414
+ ],
1415
+ )
1416
+ state_module.save_state(cg / "state.json", st)
1417
+
1418
+ async def mock_multi(*_args, **_kwargs):
1419
+ return None
1420
+
1421
+ from code_generator.orchestrator import cycle_loop
1422
+
1423
+ monkeypatch.setattr(cycle_loop, "run_multi_cycle", mock_multi)
1424
+ _mock_phase0_single(monkeypatch)
1425
+
1426
+ result = runner.invoke(
1427
+ app, ["generate", "--continue", "--cycle", "2", "--mode", "multi-cycle"]
1428
+ )
1429
+ assert result.exit_code == 0, result.output
1430
+
1431
+ st_after = state_module.load_state(cg / "state.json")
1432
+ assert st_after.cycles[0].status == "failed"
1433
+ assert st_after.cycles[1].status == "open"
1434
+
1435
+ def test_continue_without_failed_cycles_is_noop_on_reset(
1436
+ self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
1437
+ ) -> None:
1438
+ """--continue on a state with no failed cycles does not mutate status."""
1439
+ monkeypatch.chdir(tmp_path)
1440
+
1441
+ cg = tmp_path / ".code-generator"
1442
+ cg.mkdir(parents=True, exist_ok=True)
1443
+ st = state_module.State(
1444
+ mode="multi-cycle", # type: ignore[arg-type]
1445
+ started_at="2026-01-01T00:00:00Z",
1446
+ updated_at="2026-01-01T00:00:00Z",
1447
+ current_cycle=1,
1448
+ phase=None,
1449
+ cycles=[_make_cycle(cycle_id=1, phase=3, status="open")],
1450
+ )
1451
+ state_module.save_state(cg / "state.json", st)
1452
+
1453
+ async def mock_multi(*_args, **_kwargs):
1454
+ return None
1455
+
1456
+ from code_generator.orchestrator import cycle_loop
1457
+
1458
+ monkeypatch.setattr(cycle_loop, "run_multi_cycle", mock_multi)
1459
+ _mock_phase0_single(monkeypatch)
1460
+
1461
+ result = runner.invoke(app, ["generate", "--continue"])
1462
+ assert result.exit_code == 0, result.output
1463
+
1464
+ st_after = state_module.load_state(cg / "state.json")
1465
+ assert st_after.cycles[0].status == "open"
@@ -504,3 +504,77 @@ class TestPhase2TokenAccumulation:
504
504
 
505
505
  assert cycle.token_usage.get("phase2") == issue_usage
506
506
  assert "phase2" not in st.token_usage
507
+
508
+ @pytest.mark.asyncio
509
+ async def test_skips_already_reviewed_issues(self, tmp_path: Path) -> None:
510
+ """Issues with reviewed=True from a prior --continue run must be skipped."""
511
+ state_dir = tmp_path / ".code-generator"
512
+ state_dir.mkdir(parents=True, exist_ok=True)
513
+ st = _state.load_state(state_dir / "missing.json")
514
+ st.issues = [
515
+ _state.IssueState(number=1, status="open", agent="python-pro", reviewed=True),
516
+ _state.IssueState(number=2, status="open", agent="python-pro", reviewed=False),
517
+ _state.IssueState(number=3, status="open", agent="python-pro", reviewed=True),
518
+ ]
519
+ _state.save_state(state_dir / "state.json", st)
520
+
521
+ reviewed_numbers: list[int] = []
522
+
523
+ async def fake_with_backoff(factory, *, breaker, **kw):
524
+ await factory()
525
+
526
+ async def fake_main_loop(runner, prompt, options, *, state_path, logger, **kw):
527
+ return _make_run_result()
528
+
529
+ with (
530
+ patch.object(phase2_review.rate_limit, "main_loop", side_effect=fake_main_loop),
531
+ patch.object(phase2_review.retry, "with_backoff", side_effect=fake_with_backoff),
532
+ patch.object(phase2_review.gh, "view_issue", return_value={"comments": []}),
533
+ patch.object(phase2_review, "load_prompt", side_effect=lambda *a, **k: "prompt"),
534
+ patch.object(
535
+ phase2_review,
536
+ "fetch_formatted_comments",
537
+ side_effect=lambda n, logger: reviewed_numbers.append(n) or "",
538
+ ),
539
+ ):
540
+ await phase2_review.run(
541
+ st,
542
+ None,
543
+ tmp_path,
544
+ runner_module=MagicMock(),
545
+ logger=logging.getLogger("test"),
546
+ )
547
+
548
+ # Only issue #2 is actually reviewed; #1 and #3 are skipped.
549
+ assert reviewed_numbers == [2]
550
+ # Issue #2 is now marked reviewed; #1 and #3 stay reviewed.
551
+ assert all(i.reviewed for i in st.issues)
552
+
553
+ @pytest.mark.asyncio
554
+ async def test_successful_review_persists_reviewed_flag(self, tmp_path: Path) -> None:
555
+ """A successful Phase 2 review must persist reviewed=True to state.json."""
556
+ state = _make_state_with_issues(tmp_path, [42])
557
+ state_path = tmp_path / ".code-generator" / "state.json"
558
+
559
+ async def fake_with_backoff(factory, *, breaker, **kw):
560
+ await factory()
561
+
562
+ async def fake_main_loop(runner, prompt, options, *, state_path, logger, **kw):
563
+ return _make_run_result()
564
+
565
+ with (
566
+ patch.object(phase2_review.rate_limit, "main_loop", side_effect=fake_main_loop),
567
+ patch.object(phase2_review.retry, "with_backoff", side_effect=fake_with_backoff),
568
+ patch.object(phase2_review.gh, "view_issue", return_value={"comments": []}),
569
+ ):
570
+ await phase2_review.run(
571
+ state,
572
+ None,
573
+ tmp_path,
574
+ runner_module=MagicMock(),
575
+ logger=logging.getLogger("test"),
576
+ )
577
+
578
+ # Reloaded state must show reviewed=True so a subsequent --continue skips it.
579
+ reloaded = _state.load_state(state_path)
580
+ assert reloaded.issues[0].reviewed is True