drydock-cli 3.0.128__tar.gz → 3.0.130__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 (141) hide show
  1. {drydock_cli-3.0.128/drydock_cli.egg-info → drydock_cli-3.0.130}/PKG-INFO +1 -1
  2. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/drydock/agent.py +9 -0
  3. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/drydock/task_state.py +58 -0
  4. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/drydock/tui/app.py +4 -1
  5. {drydock_cli-3.0.128 → drydock_cli-3.0.130/drydock_cli.egg-info}/PKG-INFO +1 -1
  6. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/drydock_cli.egg-info/SOURCES.txt +1 -0
  7. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/pyproject.toml +1 -1
  8. drydock_cli-3.0.130/tests/test_rolling_plan.py +49 -0
  9. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_task_state.py +22 -0
  10. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/LICENSE +0 -0
  11. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/NOTICE +0 -0
  12. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/README.md +0 -0
  13. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/drydock/__init__.py +0 -0
  14. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/drydock/__main__.py +0 -0
  15. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/drydock/advisor.py +0 -0
  16. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/drydock/bash_safety.py +0 -0
  17. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/drydock/builtin_skills/__init__.py +0 -0
  18. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/drydock/builtin_skills/ml-data.md +0 -0
  19. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/drydock/builtin_skills/ml-debug.md +0 -0
  20. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/drydock/builtin_skills/ml-finetune.md +0 -0
  21. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/drydock/builtin_skills/ml-metrics.md +0 -0
  22. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/drydock/builtin_skills/ml-rl.md +0 -0
  23. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/drydock/builtin_skills/ml-train.md +0 -0
  24. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/drydock/builtin_skills/rmf-categorize.md +0 -0
  25. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/drydock/builtin_skills/rmf-control.md +0 -0
  26. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/drydock/builtin_skills/rmf-poam.md +0 -0
  27. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/drydock/builtin_skills/rmf-review.md +0 -0
  28. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/drydock/builtin_skills/stig-assess.md +0 -0
  29. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/drydock/builtin_skills/stig-remediate.md +0 -0
  30. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/drydock/cci.py +0 -0
  31. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/drydock/cli.py +0 -0
  32. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/drydock/compaction.py +0 -0
  33. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/drydock/config.py +0 -0
  34. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/drydock/detect.py +0 -0
  35. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/drydock/events.py +0 -0
  36. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/drydock/extract.py +0 -0
  37. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/drydock/gittools.py +0 -0
  38. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/drydock/graphrag.py +0 -0
  39. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/drydock/guards.py +0 -0
  40. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/drydock/loop_detect.py +0 -0
  41. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/drydock/mcp.py +0 -0
  42. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/drydock/poam.py +0 -0
  43. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/drydock/providers.py +0 -0
  44. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/drydock/recipes.py +0 -0
  45. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/drydock/rmf.py +0 -0
  46. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/drydock/rmf_graph.py +0 -0
  47. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/drydock/skills.py +0 -0
  48. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/drydock/stig.py +0 -0
  49. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/drydock/suggest.py +0 -0
  50. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/drydock/tool_registry.py +0 -0
  51. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/drydock/tools/__init__.py +0 -0
  52. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/drydock/tui/__init__.py +0 -0
  53. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/drydock/tui/approval.py +0 -0
  54. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/drydock/tui/messages.py +0 -0
  55. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/drydock/tui/widgets.py +0 -0
  56. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/drydock/tuning.py +0 -0
  57. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/drydock/verification.py +0 -0
  58. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/drydock/web.py +0 -0
  59. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/drydock_cli.egg-info/dependency_links.txt +0 -0
  60. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/drydock_cli.egg-info/entry_points.txt +0 -0
  61. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/drydock_cli.egg-info/requires.txt +0 -0
  62. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/drydock_cli.egg-info/top_level.txt +0 -0
  63. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/setup.cfg +0 -0
  64. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_advisor.py +0 -0
  65. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_approval.py +0 -0
  66. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_back_command.py +0 -0
  67. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_bash_background.py +0 -0
  68. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_bash_binary_output.py +0 -0
  69. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_bash_crossplatform.py +0 -0
  70. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_bash_output_bounding.py +0 -0
  71. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_bash_process_group.py +0 -0
  72. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_bash_safety.py +0 -0
  73. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_bash_sanitize.py +0 -0
  74. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_bash_shell.py +0 -0
  75. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_bash_stdin.py +0 -0
  76. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_bash_stop_partial.py +0 -0
  77. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_bash_timeout_network.py +0 -0
  78. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_bash_timeout_param.py +0 -0
  79. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_cci.py +0 -0
  80. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_cli_agents.py +0 -0
  81. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_compact_command.py +0 -0
  82. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_compaction.py +0 -0
  83. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_config.py +0 -0
  84. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_config_migration.py +0 -0
  85. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_context_limit_config.py +0 -0
  86. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_detect.py +0 -0
  87. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_dispatch.py +0 -0
  88. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_e2e_connected.py +0 -0
  89. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_edit_replace_all.py +0 -0
  90. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_empty_response.py +0 -0
  91. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_events.py +0 -0
  92. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_extract.py +0 -0
  93. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_failure_loop.py +0 -0
  94. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_first_run_setup.py +0 -0
  95. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_gittools.py +0 -0
  96. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_glob_edit_edges.py +0 -0
  97. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_graphify_example.py +0 -0
  98. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_graphrag.py +0 -0
  99. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_graphrag_quoted_path.py +0 -0
  100. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_grep_and_read_robust.py +0 -0
  101. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_guards_and_tools.py +0 -0
  102. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_hallucinated_tools.py +0 -0
  103. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_leaked_tool_call.py +0 -0
  104. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_loop_detect.py +0 -0
  105. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_mcp.py +0 -0
  106. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_oneshot_unreachable.py +0 -0
  107. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_overthink_interrupt.py +0 -0
  108. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_plan_autocontinue.py +0 -0
  109. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_poam.py +0 -0
  110. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_providers_unreachable.py +0 -0
  111. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_read_index.py +0 -0
  112. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_recipes.py +0 -0
  113. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_repetition_interrupt.py +0 -0
  114. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_rmf.py +0 -0
  115. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_rmf_graph.py +0 -0
  116. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_rmf_stig_graph.py +0 -0
  117. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_runaway_repetition.py +0 -0
  118. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_screenshot.py +0 -0
  119. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_server_probe.py +0 -0
  120. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_skills.py +0 -0
  121. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_stall_retry.py +0 -0
  122. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_stig.py +0 -0
  123. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_stop.py +0 -0
  124. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_streaming_newlines.py +0 -0
  125. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_subagent.py +0 -0
  126. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_suggest.py +0 -0
  127. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_system_prompt_help.py +0 -0
  128. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_todo.py +0 -0
  129. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_tool_arg_coercion.py +0 -0
  130. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_tool_arg_coercion_more.py +0 -0
  131. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_tool_arg_parsing.py +0 -0
  132. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_tools_undo.py +0 -0
  133. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_tui.py +0 -0
  134. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_tuning.py +0 -0
  135. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_verification_gate.py +0 -0
  136. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_viewimage.py +0 -0
  137. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_vision_input.py +0 -0
  138. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_web_tools.py +0 -0
  139. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_windows_shell.py +0 -0
  140. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_write_content_coerce.py +0 -0
  141. {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_xccdf.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: drydock-cli
3
- Version: 3.0.128
3
+ Version: 3.0.130
4
4
  Summary: Drydock — a local, provider-agnostic terminal coding agent for local LLMs
5
5
  Author: Frank Bobe III
6
6
  License-Expression: Apache-2.0
@@ -389,6 +389,8 @@ def run(
389
389
  for tc in assistant_turn.tool_calls:
390
390
  tool_call_count += 1
391
391
  if tc["name"] in ("Edit", "Write"):
392
+ if not session_has_edited and state.task.phase in ("understand", "discover", "plan"):
393
+ state.task.phase = "implement" # first change → building
392
394
  session_has_edited = True
393
395
 
394
396
  # STOP pressed: don't run the remaining tools, but still record a
@@ -450,6 +452,13 @@ def run(
450
452
  _emit(state, "verification", status=last_verification.status,
451
453
  exit_code=last_verification.exit_code)
452
454
 
455
+ # Sync the rolling plan when the model updates its checklist, keeping
456
+ # stable step ids + capping pending steps; record each revision.
457
+ if tc["name"] == "todo" and isinstance(config.get("_todo"), list):
458
+ if state.task.plan.update_from_items(config["_todo"]):
459
+ _emit(state, "plan", version=state.task.plan.version,
460
+ steps=[(s["id"], s["status"]) for s in state.task.plan.steps])
461
+
453
462
  _emit(state, "tool", name=tc["name"],
454
463
  input=str(tc.get("input"))[:200],
455
464
  result_chars=len(str(result)),
@@ -40,12 +40,64 @@ def extract_acceptance_criteria(objective: str, limit: int = 10) -> list[str]:
40
40
  return out[:limit]
41
41
 
42
42
 
43
+ def _step_id(text: str) -> str:
44
+ """Stable short id for a plan step, derived from its text (so the SAME step
45
+ keeps its id across plan revisions). Deterministic — no randomness/time."""
46
+ h = 0
47
+ for ch in text.strip().lower():
48
+ h = (h * 131 + ord(ch)) & 0xFFFFFFFF
49
+ return f"s{h:08x}"
50
+
51
+
52
+ @dataclass
53
+ class RollingPlan:
54
+ """A short, updateable plan (PRD Epic C): one ACTIVE step + a few pending ones,
55
+ each with a stable id. Rebuilt from the checklist the model maintains; ids are
56
+ text-derived so an unchanged step keeps its id across revisions."""
57
+ steps: list[dict] = field(default_factory=list) # {id, text, status}
58
+ version: int = 0
59
+ max_pending: int = 4
60
+
61
+ def update_from_items(self, items: list[tuple[str, str]]) -> bool:
62
+ """Sync from (text, status) checklist pairs. Caps pending steps (keeps the
63
+ active + first max_pending pending). Returns True if the plan CHANGED."""
64
+ steps = [{"id": _step_id(t), "text": t, "status": s} for t, s in items if t]
65
+ # active = first in_progress, else first pending
66
+ active = next((s for s in steps if s["status"] == "in_progress"), None)
67
+ if active is None:
68
+ active = next((s for s in steps if s["status"] == "pending"), None)
69
+ kept, pending_kept = [], 0
70
+ for s in steps:
71
+ if s["status"] == "done" or s is active:
72
+ kept.append(s)
73
+ elif s["status"] == "pending" and pending_kept < self.max_pending:
74
+ kept.append(s); pending_kept += 1
75
+ elif s["status"] == "in_progress":
76
+ kept.append(s)
77
+ changed = [(s["id"], s["status"]) for s in kept] != [(s["id"], s["status"]) for s in self.steps]
78
+ if changed:
79
+ self.steps = kept
80
+ self.version += 1
81
+ return changed
82
+
83
+ def active_step(self) -> dict | None:
84
+ return next((s for s in self.steps if s["status"] == "in_progress"), None) \
85
+ or next((s for s in self.steps if s["status"] == "pending"), None)
86
+
87
+ def remaining(self) -> int:
88
+ return sum(1 for s in self.steps if s["status"] != "done")
89
+
90
+ def to_dict(self) -> dict:
91
+ return {"steps": list(self.steps), "version": self.version}
92
+
93
+
43
94
  @dataclass
44
95
  class TaskState:
45
96
  """Authoritative task facts, independent of the message transcript."""
46
97
  objective: str = ""
47
98
  acceptance_criteria: list[str] = field(default_factory=list)
48
99
  phase: str = "understand"
100
+ plan: RollingPlan = field(default_factory=RollingPlan)
49
101
 
50
102
  @classmethod
51
103
  def from_objective(cls, objective: str) -> TaskState:
@@ -61,14 +113,20 @@ class TaskState:
61
113
  "objective": self.objective,
62
114
  "acceptance_criteria": list(self.acceptance_criteria),
63
115
  "phase": self.phase,
116
+ "plan": self.plan.to_dict(),
64
117
  }
65
118
 
66
119
  @classmethod
67
120
  def from_dict(cls, d: dict) -> TaskState:
121
+ plan = RollingPlan()
122
+ pd = d.get("plan") or {}
123
+ plan.steps = list(pd.get("steps", []))
124
+ plan.version = pd.get("version", 0)
68
125
  return cls(
69
126
  objective=d.get("objective", ""),
70
127
  acceptance_criteria=list(d.get("acceptance_criteria", [])),
71
128
  phase=d.get("phase", "understand"),
129
+ plan=plan,
72
130
  )
73
131
 
74
132
  def anchor_text(self) -> str:
@@ -362,8 +362,11 @@ class DrydockApp(App):
362
362
  flag = "⚓ working" if self._busy else "⚓ ready"
363
363
  # Busy → show the interrupt keys; idle → the quit hint.
364
364
  keys = "Esc/Ctrl+C stop" if self._busy else "Ctrl+C×2 quit"
365
+ # Show the task phase once it's past the default (understand).
366
+ ph = getattr(self.state, "task", None)
367
+ phase = f" · {ph.phase}" if (ph and ph.is_set() and ph.phase != "understand") else ""
365
368
  return (
366
- f"{flag} · {model} · {keys} · PgUp/PgDn scroll · "
369
+ f"{flag} · {model}{phase} · {keys} · PgUp/PgDn scroll · "
367
370
  f"Ctrl+O details · {ctx}"
368
371
  )
369
372
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: drydock-cli
3
- Version: 3.0.128
3
+ Version: 3.0.130
4
4
  Summary: Drydock — a local, provider-agnostic terminal coding agent for local LLMs
5
5
  Author: Frank Bobe III
6
6
  License-Expression: Apache-2.0
@@ -110,6 +110,7 @@ tests/test_repetition_interrupt.py
110
110
  tests/test_rmf.py
111
111
  tests/test_rmf_graph.py
112
112
  tests/test_rmf_stig_graph.py
113
+ tests/test_rolling_plan.py
113
114
  tests/test_runaway_repetition.py
114
115
  tests/test_screenshot.py
115
116
  tests/test_server_probe.py
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
7
7
  # PyPI distribution name is drydock-cli (the established install name, continued
8
8
  # from the retired v2 fork); the import package + CLI command stay `drydock`.
9
9
  name = "drydock-cli"
10
- version = "3.0.128"
10
+ version = "3.0.130"
11
11
  description = "Drydock — a local, provider-agnostic terminal coding agent for local LLMs"
12
12
  readme = "README.md"
13
13
  requires-python = ">=3.11"
@@ -0,0 +1,49 @@
1
+ """Rolling plan (Agent-Buildout PRD Epic C): 1 active + <=4 pending steps with
2
+ stable ids; revisions bump the version; unchanged steps keep their id."""
3
+ from __future__ import annotations
4
+
5
+ from drydock.task_state import RollingPlan, TaskState, _step_id
6
+
7
+
8
+ def test_stable_ids_survive_revision():
9
+ p = RollingPlan()
10
+ p.update_from_items([("read file", "done"), ("fix bug", "in_progress"), ("test", "pending")])
11
+ ids1 = {s["text"]: s["id"] for s in p.steps}
12
+ # revise: fix bug now done, test now active
13
+ p.update_from_items([("read file", "done"), ("fix bug", "done"), ("test", "in_progress")])
14
+ ids2 = {s["text"]: s["id"] for s in p.steps}
15
+ assert ids1["fix bug"] == ids2["fix bug"] # same step, same id
16
+ assert ids1["test"] == ids2["test"]
17
+
18
+
19
+ def test_caps_pending_to_four_plus_active():
20
+ p = RollingPlan()
21
+ items = [("active", "in_progress")] + [(f"p{i}", "pending") for i in range(8)]
22
+ p.update_from_items(items)
23
+ pend = [s for s in p.steps if s["status"] == "pending"]
24
+ assert len(pend) == 4 and any(s["status"] == "in_progress" for s in p.steps)
25
+
26
+
27
+ def test_version_bumps_only_on_change():
28
+ p = RollingPlan()
29
+ assert p.update_from_items([("a", "pending")]) and p.version == 1
30
+ assert not p.update_from_items([("a", "pending")]) and p.version == 1 # no change
31
+ assert p.update_from_items([("a", "done")]) and p.version == 2
32
+
33
+
34
+ def test_active_and_remaining():
35
+ p = RollingPlan()
36
+ p.update_from_items([("a", "done"), ("b", "in_progress"), ("c", "pending")])
37
+ assert p.active_step()["text"] == "b" and p.remaining() == 2
38
+
39
+
40
+ def test_serialization_roundtrip():
41
+ ts = TaskState.from_objective("Do it.\n1. a\n2. b")
42
+ ts.plan.update_from_items([("a", "done"), ("b", "in_progress")])
43
+ d = ts.to_dict()
44
+ assert d["plan"]["version"] == 1 and len(d["plan"]["steps"]) == 2
45
+ assert TaskState.from_dict(d).plan.active_step()["text"] == "b"
46
+
47
+
48
+ def test_step_id_deterministic():
49
+ assert _step_id("fix the bug") == _step_id("Fix The Bug ") # case/space-insensitive
@@ -67,3 +67,25 @@ def test_disabled_when_task_anchor_false():
67
67
  cfg = {"model": "gemma4", "max_tokens": 8192, "context_limit": 65536, "task_anchor": False}
68
68
  sysp, _ = _run_capture(cfg, "Do the thing.\n1. a criterion")
69
69
  assert "TASK" not in sysp
70
+
71
+
72
+ def test_phase_transitions_understand_implement_complete():
73
+ from drydock.providers import AssistantTurn
74
+ cfg = {"model": "gemma4", "max_tokens": 8192, "context_limit": 65536}
75
+ turns = [
76
+ AssistantTurn("", [{"id": "1", "name": "Write",
77
+ "input": {"file_path": "/tmp/dd_ph.txt", "content": "x"}}], 5, 5),
78
+ AssistantTurn("", [{"id": "2", "name": "Bash", "input": {"command": "python -c 'print(1)'"}}], 5, 5),
79
+ AssistantTurn("done, verified", [], 5, 5),
80
+ ]
81
+ i = {"n": 0}
82
+ def fake(model, system, messages, tool_schemas, config):
83
+ t = turns[min(i["n"], len(turns) - 1)]; i["n"] += 1; yield t
84
+ st = agent.AgentState()
85
+ assert st.task.phase == "understand"
86
+ orig = agent.stream; agent.stream = fake
87
+ try:
88
+ list(agent.run("Fix it and verify.\n1. tests pass", st, cfg, "SYS"))
89
+ finally:
90
+ agent.stream = orig
91
+ assert st.task.phase == "complete" # understand -> implement -> verify -> complete
File without changes
File without changes
File without changes
File without changes