drydock-cli 3.0.129__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.129/drydock_cli.egg-info → drydock_cli-3.0.130}/PKG-INFO +1 -1
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/drydock/agent.py +7 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/drydock/task_state.py +58 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130/drydock_cli.egg-info}/PKG-INFO +1 -1
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/drydock_cli.egg-info/SOURCES.txt +1 -0
- {drydock_cli-3.0.129 → 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.129 → drydock_cli-3.0.130}/LICENSE +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/NOTICE +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/README.md +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/drydock/__init__.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/drydock/__main__.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/drydock/advisor.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/drydock/bash_safety.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/drydock/builtin_skills/__init__.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/drydock/builtin_skills/ml-data.md +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/drydock/builtin_skills/ml-debug.md +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/drydock/builtin_skills/ml-finetune.md +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/drydock/builtin_skills/ml-metrics.md +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/drydock/builtin_skills/ml-rl.md +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/drydock/builtin_skills/ml-train.md +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/drydock/builtin_skills/rmf-categorize.md +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/drydock/builtin_skills/rmf-control.md +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/drydock/builtin_skills/rmf-poam.md +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/drydock/builtin_skills/rmf-review.md +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/drydock/builtin_skills/stig-assess.md +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/drydock/builtin_skills/stig-remediate.md +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/drydock/cci.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/drydock/cli.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/drydock/compaction.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/drydock/config.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/drydock/detect.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/drydock/events.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/drydock/extract.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/drydock/gittools.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/drydock/graphrag.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/drydock/guards.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/drydock/loop_detect.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/drydock/mcp.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/drydock/poam.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/drydock/providers.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/drydock/recipes.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/drydock/rmf.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/drydock/rmf_graph.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/drydock/skills.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/drydock/stig.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/drydock/suggest.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/drydock/tool_registry.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/drydock/tools/__init__.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/drydock/tui/__init__.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/drydock/tui/app.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/drydock/tui/approval.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/drydock/tui/messages.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/drydock/tui/widgets.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/drydock/tuning.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/drydock/verification.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/drydock/web.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/drydock_cli.egg-info/dependency_links.txt +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/drydock_cli.egg-info/entry_points.txt +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/drydock_cli.egg-info/requires.txt +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/drydock_cli.egg-info/top_level.txt +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/setup.cfg +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/tests/test_advisor.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/tests/test_approval.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/tests/test_back_command.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/tests/test_bash_background.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/tests/test_bash_binary_output.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/tests/test_bash_crossplatform.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/tests/test_bash_output_bounding.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/tests/test_bash_process_group.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/tests/test_bash_safety.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/tests/test_bash_sanitize.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/tests/test_bash_shell.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/tests/test_bash_stdin.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/tests/test_bash_stop_partial.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/tests/test_bash_timeout_network.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/tests/test_bash_timeout_param.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/tests/test_cci.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/tests/test_cli_agents.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/tests/test_compact_command.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/tests/test_compaction.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/tests/test_config.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/tests/test_config_migration.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/tests/test_context_limit_config.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/tests/test_detect.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/tests/test_dispatch.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/tests/test_e2e_connected.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/tests/test_edit_replace_all.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/tests/test_empty_response.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/tests/test_events.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/tests/test_extract.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/tests/test_failure_loop.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/tests/test_first_run_setup.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/tests/test_gittools.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/tests/test_glob_edit_edges.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/tests/test_graphify_example.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/tests/test_graphrag.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/tests/test_graphrag_quoted_path.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/tests/test_grep_and_read_robust.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/tests/test_guards_and_tools.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/tests/test_hallucinated_tools.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/tests/test_leaked_tool_call.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/tests/test_loop_detect.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/tests/test_mcp.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/tests/test_oneshot_unreachable.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/tests/test_overthink_interrupt.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/tests/test_plan_autocontinue.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/tests/test_poam.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/tests/test_providers_unreachable.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/tests/test_read_index.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/tests/test_recipes.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/tests/test_repetition_interrupt.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/tests/test_rmf.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/tests/test_rmf_graph.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/tests/test_rmf_stig_graph.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/tests/test_runaway_repetition.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/tests/test_screenshot.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/tests/test_server_probe.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/tests/test_skills.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/tests/test_stall_retry.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/tests/test_stig.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/tests/test_stop.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/tests/test_streaming_newlines.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/tests/test_subagent.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/tests/test_suggest.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/tests/test_system_prompt_help.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/tests/test_task_state.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/tests/test_todo.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/tests/test_tool_arg_coercion.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/tests/test_tool_arg_coercion_more.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/tests/test_tool_arg_parsing.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/tests/test_tools_undo.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/tests/test_tui.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/tests/test_tuning.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/tests/test_verification_gate.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/tests/test_viewimage.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/tests/test_vision_input.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/tests/test_web_tools.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/tests/test_windows_shell.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/tests/test_write_content_coerce.py +0 -0
- {drydock_cli-3.0.129 → drydock_cli-3.0.130}/tests/test_xccdf.py +0 -0
|
@@ -452,6 +452,13 @@ def run(
|
|
|
452
452
|
_emit(state, "verification", status=last_verification.status,
|
|
453
453
|
exit_code=last_verification.exit_code)
|
|
454
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
|
+
|
|
455
462
|
_emit(state, "tool", name=tc["name"],
|
|
456
463
|
input=str(tc.get("input"))[:200],
|
|
457
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:
|
|
@@ -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
|
|
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
|
|
File without changes
|
|
File without changes
|