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.
- {drydock_cli-3.0.128/drydock_cli.egg-info → drydock_cli-3.0.130}/PKG-INFO +1 -1
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/drydock/agent.py +9 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/drydock/task_state.py +58 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/drydock/tui/app.py +4 -1
- {drydock_cli-3.0.128 → drydock_cli-3.0.130/drydock_cli.egg-info}/PKG-INFO +1 -1
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/drydock_cli.egg-info/SOURCES.txt +1 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/pyproject.toml +1 -1
- drydock_cli-3.0.130/tests/test_rolling_plan.py +49 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_task_state.py +22 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/LICENSE +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/NOTICE +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/README.md +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/drydock/__init__.py +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/drydock/__main__.py +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/drydock/advisor.py +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/drydock/bash_safety.py +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/drydock/builtin_skills/__init__.py +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/drydock/builtin_skills/ml-data.md +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/drydock/builtin_skills/ml-debug.md +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/drydock/builtin_skills/ml-finetune.md +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/drydock/builtin_skills/ml-metrics.md +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/drydock/builtin_skills/ml-rl.md +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/drydock/builtin_skills/ml-train.md +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/drydock/builtin_skills/rmf-categorize.md +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/drydock/builtin_skills/rmf-control.md +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/drydock/builtin_skills/rmf-poam.md +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/drydock/builtin_skills/rmf-review.md +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/drydock/builtin_skills/stig-assess.md +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/drydock/builtin_skills/stig-remediate.md +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/drydock/cci.py +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/drydock/cli.py +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/drydock/compaction.py +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/drydock/config.py +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/drydock/detect.py +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/drydock/events.py +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/drydock/extract.py +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/drydock/gittools.py +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/drydock/graphrag.py +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/drydock/guards.py +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/drydock/loop_detect.py +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/drydock/mcp.py +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/drydock/poam.py +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/drydock/providers.py +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/drydock/recipes.py +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/drydock/rmf.py +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/drydock/rmf_graph.py +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/drydock/skills.py +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/drydock/stig.py +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/drydock/suggest.py +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/drydock/tool_registry.py +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/drydock/tools/__init__.py +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/drydock/tui/__init__.py +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/drydock/tui/approval.py +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/drydock/tui/messages.py +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/drydock/tui/widgets.py +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/drydock/tuning.py +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/drydock/verification.py +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/drydock/web.py +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/drydock_cli.egg-info/dependency_links.txt +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/drydock_cli.egg-info/entry_points.txt +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/drydock_cli.egg-info/requires.txt +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/drydock_cli.egg-info/top_level.txt +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/setup.cfg +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_advisor.py +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_approval.py +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_back_command.py +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_bash_background.py +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_bash_binary_output.py +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_bash_crossplatform.py +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_bash_output_bounding.py +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_bash_process_group.py +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_bash_safety.py +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_bash_sanitize.py +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_bash_shell.py +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_bash_stdin.py +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_bash_stop_partial.py +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_bash_timeout_network.py +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_bash_timeout_param.py +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_cci.py +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_cli_agents.py +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_compact_command.py +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_compaction.py +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_config.py +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_config_migration.py +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_context_limit_config.py +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_detect.py +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_dispatch.py +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_e2e_connected.py +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_edit_replace_all.py +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_empty_response.py +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_events.py +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_extract.py +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_failure_loop.py +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_first_run_setup.py +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_gittools.py +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_glob_edit_edges.py +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_graphify_example.py +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_graphrag.py +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_graphrag_quoted_path.py +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_grep_and_read_robust.py +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_guards_and_tools.py +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_hallucinated_tools.py +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_leaked_tool_call.py +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_loop_detect.py +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_mcp.py +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_oneshot_unreachable.py +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_overthink_interrupt.py +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_plan_autocontinue.py +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_poam.py +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_providers_unreachable.py +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_read_index.py +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_recipes.py +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_repetition_interrupt.py +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_rmf.py +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_rmf_graph.py +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_rmf_stig_graph.py +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_runaway_repetition.py +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_screenshot.py +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_server_probe.py +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_skills.py +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_stall_retry.py +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_stig.py +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_stop.py +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_streaming_newlines.py +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_subagent.py +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_suggest.py +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_system_prompt_help.py +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_todo.py +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_tool_arg_coercion.py +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_tool_arg_coercion_more.py +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_tool_arg_parsing.py +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_tools_undo.py +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_tui.py +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_tuning.py +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_verification_gate.py +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_viewimage.py +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_vision_input.py +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_web_tools.py +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_windows_shell.py +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_write_content_coerce.py +0 -0
- {drydock_cli-3.0.128 → drydock_cli-3.0.130}/tests/test_xccdf.py +0 -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
|
|
|
@@ -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.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|