fn-13-pxj.4 Add flowctl task reset command
Description
Add flowctl task reset <task_id> [--cascade] to reset task status to todo.
Implementation
In flowctl.py:
Add cmd_task_reset():
def cmd_task_reset(args):
task = load_task(args.task_id)
epic = load_epic(task["epic"])
# Validations
if epic["status"] == "done":
error_exit("Cannot reset task in closed epic", args.json)
if task["status"] == "in_progress":
error_exit("Cannot reset in_progress task", args.json)
if task["status"] == "todo":
# Already pending, no-op success
json_output({"success": True, "reset": [], "message": "Already todo"}) if args.json else print("Already todo")
return
# Reset status
task["status"] = "todo"
task["updated_at"] = now_iso()
# Clear optional fields if present
task.pop("blocked_reason", None)
task.pop("completed_at", None)
# Clear claim fields (CRITICAL for multi-user)
task.pop("assignee", None)
task.pop("claimed_at", None)
task.pop("claim_note", None)
# Clear evidence from JSON (if present)
task.pop("evidence", None)
save_task(task)
# Clear evidence CONTENTS from task spec markdown (keep heading!)
clear_task_evidence(args.task_id)
reset_ids = [args.task_id]
if args.cascade:
# Find and reset dependent tasks (same epic only)
dependents = find_dependents(args.task_id, same_epic=True)
for dep_id in dependents:
dep_task = load_task(dep_id)
if dep_task["status"] == "in_progress":
continue # Skip in_progress dependents
if dep_task["status"] == "todo":
continue # Already todo
dep_task["status"] = "todo"
dep_task["updated_at"] = now_iso()
dep_task.pop("blocked_reason", None)
dep_task.pop("completed_at", None)
dep_task.pop("assignee", None)
dep_task.pop("claimed_at", None)
dep_task.pop("claim_note", None)
dep_task.pop("evidence", None)
save_task(dep_task)
clear_task_evidence(dep_id)
reset_ids.append(dep_id)
# Output
json_output({"success": True, "reset": reset_ids}) if args.json else print(f"Reset: {', '.join(reset_ids)}")
Add clear_task_evidence() helper:
def clear_task_evidence(task_id: str) -> None:
"""Clear ## Evidence section contents (but KEEP the heading)."""
spec_path = get_task_spec_path(task_id)
if not spec_path.exists():
return
content = spec_path.read_text()
# Replace contents under ## Evidence with empty template, keeping heading
# Pattern: ## Evidence\n<content until next ##> -> ## Evidence\n- Commits:\n- Tests:\n- PRs:\n
# Use regex to find ## Evidence section and replace its content
import re
pattern = r'(## Evidence\n).*?(?=\n## |\Z)'
replacement = r'\1- Commits:\n- Tests:\n- PRs:\n'
new_content = re.sub(pattern, replacement, content, flags=re.DOTALL)
if new_content != content:
spec_path.write_text(new_content)
Add find_dependents() helper:
def find_dependents(task_id: str, same_epic: bool = False) -> list[str]:
"""Find tasks that depend on task_id (recursive)."""
# Scan all tasks, find those with task_id in depends_on
# If same_epic, filter to same epic
# Recursively find their dependents too
Register parser:
p_task_reset = task_sub.add_parser("reset", help="Reset task to todo")
p_task_reset.add_argument("task_id", help="Task ID")
p_task_reset.add_argument("--cascade", action="store_true", help="Also reset dependents")
p_task_reset.add_argument("--json", action="store_true")
p_task_reset.set_defaults(func=cmd_task_reset)
Key Behavior Notes
clear_task_evidence() keeps the ## Evidence heading (required for future cmd_done patches)
- Replaces evidence content with default empty template (
- Commits:\n- Tests:\n- PRs:)
- Also clears
task["evidence"] from JSON
Key Files
plugins/flow-next/scripts/flowctl.py
Acceptance
Done summary
- Added
clear_task_evidence() helper to reset spec Evidence section
- Added
find_dependents() for recursive dependency traversal
- Added
cmd_task_reset() with --cascade support
- Clears: status→todo, evidence, claim fields, blocked_reason
Why: Enable retry/re-run of completed tasks without manual JSON editing
Verification: Manual tests for reset, already-todo, in_progress error; CI 31/31
Evidence
- Commits: d8fe7a45132450286a9e6777f3bc98e8874c9b1f
- Tests: plugins/flow-next/scripts/ci_test.sh
- PRs:
1---2name: 176-fn-13-pxj4-12741c653description: fn-13-pxj.4 Add flowctl task reset command4---5# fn-13-pxj.4 Add flowctl task reset command67## Description8Add `flowctl task reset <task_id> [--cascade]` to reset task status to todo.910### Implementation1112**In flowctl.py:**13141. Add `cmd_task_reset()`:15 ```python16 def cmd_task_reset(args):17 task = load_task(args.task_id)18 epic = load_epic(task["epic"])19 20 # Validations21 if epic["status"] == "done":22 error_exit("Cannot reset task in closed epic", args.json)23 if task["status"] == "in_progress":24 error_exit("Cannot reset in_progress task", args.json)25 if task["status"] == "todo":26 # Already pending, no-op success27 json_output({"success": True, "reset": [], "message": "Already todo"}) if args.json else print("Already todo")28 return29 30 # Reset status31 task["status"] = "todo"32 task["updated_at"] = now_iso()33 34 # Clear optional fields if present35 task.pop("blocked_reason", None)36 task.pop("completed_at", None)37 38 # Clear claim fields (CRITICAL for multi-user)39 task.pop("assignee", None)40 task.pop("claimed_at", None)41 task.pop("claim_note", None)42 43 # Clear evidence from JSON (if present)44 task.pop("evidence", None)45 46 save_task(task)47 48 # Clear evidence CONTENTS from task spec markdown (keep heading!)49 clear_task_evidence(args.task_id)50 51 reset_ids = [args.task_id]52 53 if args.cascade:54 # Find and reset dependent tasks (same epic only)55 dependents = find_dependents(args.task_id, same_epic=True)56 for dep_id in dependents:57 dep_task = load_task(dep_id)58 if dep_task["status"] == "in_progress":59 continue # Skip in_progress dependents60 if dep_task["status"] == "todo":61 continue # Already todo62 dep_task["status"] = "todo"63 dep_task["updated_at"] = now_iso()64 dep_task.pop("blocked_reason", None)65 dep_task.pop("completed_at", None)66 dep_task.pop("assignee", None)67 dep_task.pop("claimed_at", None)68 dep_task.pop("claim_note", None)69 dep_task.pop("evidence", None)70 save_task(dep_task)71 clear_task_evidence(dep_id)72 reset_ids.append(dep_id)73 74 # Output75 json_output({"success": True, "reset": reset_ids}) if args.json else print(f"Reset: {', '.join(reset_ids)}")76 ```77782. Add `clear_task_evidence()` helper:79 ```python80 def clear_task_evidence(task_id: str) -> None:81 """Clear ## Evidence section contents (but KEEP the heading)."""82 spec_path = get_task_spec_path(task_id)83 if not spec_path.exists():84 return85 content = spec_path.read_text()86 87 # Replace contents under ## Evidence with empty template, keeping heading88 # Pattern: ## Evidence\n<content until next ##> -> ## Evidence\n- Commits:\n- Tests:\n- PRs:\n89 # Use regex to find ## Evidence section and replace its content90 import re91 pattern = r'(## Evidence\n).*?(?=\n## |\Z)'92 replacement = r'\1- Commits:\n- Tests:\n- PRs:\n'93 new_content = re.sub(pattern, replacement, content, flags=re.DOTALL)94 95 if new_content != content:96 spec_path.write_text(new_content)97 ```98993. Add `find_dependents()` helper:100 ```python101 def find_dependents(task_id: str, same_epic: bool = False) -> list[str]:102 """Find tasks that depend on task_id (recursive)."""103 # Scan all tasks, find those with task_id in depends_on104 # If same_epic, filter to same epic105 # Recursively find their dependents too106 ```1071084. Register parser:109 ```python110 p_task_reset = task_sub.add_parser("reset", help="Reset task to todo")111 p_task_reset.add_argument("task_id", help="Task ID")112 p_task_reset.add_argument("--cascade", action="store_true", help="Also reset dependents")113 p_task_reset.add_argument("--json", action="store_true")114 p_task_reset.set_defaults(func=cmd_task_reset)115 ```116117### Key Behavior Notes118- `clear_task_evidence()` keeps the `## Evidence` heading (required for future `cmd_done` patches)119- Replaces evidence content with default empty template (`- Commits:\n- Tests:\n- PRs:`)120- Also clears `task["evidence"]` from JSON121122### Key Files123- `plugins/flow-next/scripts/flowctl.py`124## Acceptance125- [ ] `flowctl task reset <id>` changes done/blocked → todo126- [ ] Clears optional fields: blocked_reason, completed_at (if present)127- [ ] Clears claim fields: assignee, claimed_at, claim_note128- [ ] Clears ## Evidence section from task spec markdown129- [ ] Sets updated_at to current time130- [ ] `--cascade` also resets dependent tasks (same epic only)131- [ ] Cascade skips in_progress dependents132- [ ] Errors on in_progress task133- [ ] Errors on task in closed epic134- [ ] No-op (success) if task already todo135- [ ] `--json` outputs valid JSON with reset task IDs136## Done summary137- Added `clear_task_evidence()` helper to reset spec Evidence section138- Added `find_dependents()` for recursive dependency traversal139- Added `cmd_task_reset()` with --cascade support140- Clears: status→todo, evidence, claim fields, blocked_reason141142Why: Enable retry/re-run of completed tasks without manual JSON editing143144Verification: Manual tests for reset, already-todo, in_progress error; CI 31/31145## Evidence146- Commits: d8fe7a45132450286a9e6777f3bc98e8874c9b1f147- Tests: plugins/flow-next/scripts/ci_test.sh148- PRs: