🗑️ remove(progress): drop workflow state tracking
Use plan-status as the only machine state source for plan queueing, claiming, and finishing. Route record-plan through queue insertion and update templates/prompts to match the single-state model. BREAKING CHANGE: playbook.py -record-spec and main_loop.py record are removed.
This commit is contained in:
+44
-209
@@ -38,24 +38,6 @@ PLAN_META_REQUIRED_FIELDS = (
|
||||
"Verification Scope",
|
||||
"Verification Gate",
|
||||
)
|
||||
WORKFLOW_STATE_KEYS = {
|
||||
"phase",
|
||||
"spec",
|
||||
"plan",
|
||||
"executor",
|
||||
"constraints",
|
||||
"claimed_by",
|
||||
"claimed_at",
|
||||
"verification",
|
||||
}
|
||||
WORKFLOW_PHASES = {
|
||||
"brainstorming",
|
||||
"planning",
|
||||
"executing",
|
||||
"done",
|
||||
"blocked",
|
||||
"skipped",
|
||||
}
|
||||
THREAD_LOCKS: dict[str, threading.Lock] = {}
|
||||
THREAD_LOCKS_GUARD = threading.Lock()
|
||||
|
||||
@@ -67,19 +49,12 @@ def usage() -> str:
|
||||
" python scripts/main_loop.py finish -plan <path> -status <status> "
|
||||
"-progress <file> [-note <text>] [-verified <text>]\n"
|
||||
" python scripts/main_loop.py status -plans <dir> -progress <file>\n"
|
||||
" python scripts/main_loop.py record -progress <file> -phase <phase> "
|
||||
"[-spec <path>] [-plan <path>] [-executor <name>] "
|
||||
"[-constraints <csv>]\n"
|
||||
" python scripts/main_loop.py -h\n"
|
||||
"Options:\n"
|
||||
" -plans DIR\n"
|
||||
" -plan PATH\n"
|
||||
" -status done|blocked|skipped\n"
|
||||
" -progress FILE\n"
|
||||
" -phase brainstorming|planning|executing|done|blocked|skipped\n"
|
||||
" -spec PATH\n"
|
||||
" -executor NAME\n"
|
||||
" -constraints CSV\n"
|
||||
" -note TEXT\n"
|
||||
" -owner NAME\n"
|
||||
" -verified TEXT\n"
|
||||
@@ -205,11 +180,6 @@ def render_progress_lines(plans: list[str]) -> list[str]:
|
||||
lines = [
|
||||
"# 当前进展",
|
||||
"",
|
||||
"## Workflow State",
|
||||
"",
|
||||
WORKFLOW_STATE_START,
|
||||
WORKFLOW_STATE_END,
|
||||
"",
|
||||
"## Plan Status",
|
||||
"",
|
||||
PLAN_STATUS_START,
|
||||
@@ -220,47 +190,6 @@ def render_progress_lines(plans: list[str]) -> list[str]:
|
||||
return lines
|
||||
|
||||
|
||||
def render_workflow_state_lines(
|
||||
phase: Optional[str] = None,
|
||||
spec: Optional[str] = None,
|
||||
plan: Optional[str] = None,
|
||||
executor: Optional[str] = None,
|
||||
constraints: Optional[str] = None,
|
||||
claimed_by: Optional[str] = None,
|
||||
claimed_at: Optional[str] = None,
|
||||
verification: Optional[str] = None,
|
||||
) -> list[str]:
|
||||
lines = [WORKFLOW_STATE_START]
|
||||
for key, value in (
|
||||
("phase", phase),
|
||||
("spec", spec),
|
||||
("plan", plan),
|
||||
("executor", executor),
|
||||
("constraints", constraints),
|
||||
("claimed_by", claimed_by),
|
||||
("claimed_at", claimed_at),
|
||||
("verification", verification),
|
||||
):
|
||||
if value:
|
||||
lines.append(f"{key}: {value}")
|
||||
lines.append(WORKFLOW_STATE_END)
|
||||
return lines
|
||||
|
||||
|
||||
def parse_workflow_state(
|
||||
lines: list[str], start_idx: int, end_idx: int
|
||||
) -> dict[str, str]:
|
||||
state: dict[str, str] = {}
|
||||
for idx in range(start_idx + 1, end_idx):
|
||||
line = lines[idx].strip()
|
||||
if ": " not in line:
|
||||
continue
|
||||
key, value = line.split(": ", 1)
|
||||
if key in WORKFLOW_STATE_KEYS:
|
||||
state[key] = value
|
||||
return state
|
||||
|
||||
|
||||
def parse_env_blocked_note(note: Optional[str]) -> Optional[tuple[str, str]]:
|
||||
if not note:
|
||||
return None
|
||||
@@ -360,7 +289,7 @@ def ensure_block_with_lines(
|
||||
def ensure_plan_block(
|
||||
lines: list[str], progress_path: Path, plan_keys: list[str]
|
||||
) -> tuple[list[str], int, int]:
|
||||
lines, _, _ = ensure_workflow_state_block(lines)
|
||||
lines = remove_workflow_state_block(lines)
|
||||
lines, start_idx, end_idx = ensure_block_with_lines(
|
||||
lines,
|
||||
PLAN_STATUS_START,
|
||||
@@ -372,60 +301,26 @@ def ensure_plan_block(
|
||||
return lines, start_idx, end_idx
|
||||
|
||||
|
||||
def ensure_workflow_state_block(
|
||||
lines: list[str],
|
||||
) -> tuple[list[str], int, int]:
|
||||
return ensure_block_with_lines(
|
||||
lines,
|
||||
WORKFLOW_STATE_START,
|
||||
WORKFLOW_STATE_END,
|
||||
[WORKFLOW_STATE_START, WORKFLOW_STATE_END],
|
||||
"## Workflow State",
|
||||
)
|
||||
def remove_workflow_state_block(lines: list[str]) -> list[str]:
|
||||
block = find_named_block(lines, WORKFLOW_STATE_START, WORKFLOW_STATE_END)
|
||||
if not block:
|
||||
return lines
|
||||
|
||||
start_idx, end_idx = block
|
||||
remove_start = start_idx
|
||||
if (
|
||||
remove_start >= 2
|
||||
and lines[remove_start - 1].strip() == ""
|
||||
and lines[remove_start - 2].strip() == "## Workflow State"
|
||||
):
|
||||
remove_start -= 2
|
||||
if remove_start > 0 and lines[remove_start - 1].strip() == "":
|
||||
remove_start -= 1
|
||||
|
||||
def update_workflow_state(
|
||||
lines: list[str],
|
||||
phase: Optional[str] = None,
|
||||
spec: Optional[str] = None,
|
||||
plan: Optional[str] = None,
|
||||
executor: Optional[str] = None,
|
||||
constraints: Optional[str] = None,
|
||||
claimed_by: Optional[str] = None,
|
||||
claimed_at: Optional[str] = None,
|
||||
verification: Optional[str] = None,
|
||||
clear_keys: tuple[str, ...] = (),
|
||||
) -> list[str]:
|
||||
lines, start_idx, end_idx = ensure_workflow_state_block(lines)
|
||||
state = parse_workflow_state(lines, start_idx, end_idx)
|
||||
for key in clear_keys:
|
||||
state.pop(key, None)
|
||||
if phase is not None:
|
||||
state["phase"] = phase
|
||||
if spec is not None:
|
||||
state["spec"] = spec
|
||||
if plan is not None:
|
||||
state["plan"] = plan
|
||||
if executor is not None:
|
||||
state["executor"] = executor
|
||||
if constraints is not None:
|
||||
state["constraints"] = constraints
|
||||
if claimed_by is not None:
|
||||
state["claimed_by"] = claimed_by
|
||||
if claimed_at is not None:
|
||||
state["claimed_at"] = claimed_at
|
||||
if verification is not None:
|
||||
state["verification"] = verification
|
||||
lines[start_idx : end_idx + 1] = render_workflow_state_lines(
|
||||
state.get("phase"),
|
||||
state.get("spec"),
|
||||
state.get("plan"),
|
||||
state.get("executor"),
|
||||
state.get("constraints"),
|
||||
state.get("claimed_by"),
|
||||
state.get("claimed_at"),
|
||||
state.get("verification"),
|
||||
)
|
||||
remove_end = end_idx + 1
|
||||
if remove_end < len(lines) and lines[remove_end].strip() == "":
|
||||
remove_end += 1
|
||||
del lines[remove_start:remove_end]
|
||||
return lines
|
||||
|
||||
|
||||
@@ -450,6 +345,21 @@ def ensure_all_plans_present(
|
||||
return entries
|
||||
|
||||
|
||||
def ensure_plan_status_entry(lines: list[str], plan_key: str) -> list[str]:
|
||||
lines = remove_workflow_state_block(lines)
|
||||
lines, start_idx, end_idx = ensure_block_with_lines(
|
||||
lines,
|
||||
PLAN_STATUS_START,
|
||||
PLAN_STATUS_END,
|
||||
[PLAN_STATUS_START, PLAN_STATUS_END],
|
||||
"## Plan Status",
|
||||
)
|
||||
entries = parse_entries(lines, start_idx, end_idx)
|
||||
if plan_key not in {entry_plan for entry_plan, _, _, _ in entries}:
|
||||
lines[end_idx:end_idx] = [render_plan_line(plan_key, "pending", None)]
|
||||
return lines
|
||||
|
||||
|
||||
def filter_existing_entries(
|
||||
entries: list[tuple[str, str, Optional[str], int]], plan_keys: list[str]
|
||||
) -> list[tuple[str, str, Optional[str], int]]:
|
||||
@@ -483,12 +393,8 @@ def choose_claim_entry(
|
||||
current_env: Optional[str],
|
||||
plan_keys: list[str],
|
||||
) -> Optional[tuple[str, Optional[str], int]]:
|
||||
entry_by_plan: dict[str, tuple[str, str, Optional[str], int]] = {}
|
||||
for entry in entries:
|
||||
entry_by_plan.setdefault(entry[0], entry)
|
||||
ordered_entries = [
|
||||
entry_by_plan[plan_key] for plan_key in plan_keys if plan_key in entry_by_plan
|
||||
]
|
||||
available = set(plan_keys)
|
||||
ordered_entries = [entry for entry in entries if entry[0] in available]
|
||||
|
||||
for plan_key, status, note, idx in ordered_entries:
|
||||
if status == "in-progress":
|
||||
@@ -537,15 +443,12 @@ def claim_plan(
|
||||
return 0, "NOOP: no claimable plans"
|
||||
|
||||
plan_key, note, idx = chosen
|
||||
lines[idx] = render_plan_line(plan_key, "in-progress", note)
|
||||
lines = update_workflow_state(
|
||||
lines,
|
||||
phase="executing",
|
||||
plan=(plans_dir / plan_key).as_posix(),
|
||||
claimed_by=normalize_note(owner) if owner else default_claim_owner(),
|
||||
claimed_at=now_utc(),
|
||||
clear_keys=("verification",),
|
||||
claim_note = (
|
||||
f"claimed_by: {normalize_note(owner) if owner else default_claim_owner()}; "
|
||||
f"claimed_at: {now_utc()}"
|
||||
)
|
||||
updated_note = f"{note}; {claim_note}" if note else claim_note
|
||||
lines[idx] = render_plan_line(plan_key, "in-progress", updated_note)
|
||||
write_progress_lines(progress_path, lines)
|
||||
|
||||
output = [f"PLAN={(plans_dir / plan_key).as_posix()}"]
|
||||
@@ -582,7 +485,6 @@ def finish_plan(
|
||||
entries = parse_entries(lines, start_idx, end_idx)
|
||||
rendered_note = normalize_note(note) if note else None
|
||||
rendered_verified = normalize_note(verified) if verified else None
|
||||
verification_clear_keys = () if rendered_verified else ("verification",)
|
||||
if rendered_verified:
|
||||
verified_note = f"verified: {rendered_verified}"
|
||||
rendered_note = (
|
||||
@@ -591,33 +493,14 @@ def finish_plan(
|
||||
else verified_note
|
||||
)
|
||||
updated_line = render_plan_line(plan_key, status, rendered_note)
|
||||
workflow_phase = {
|
||||
"done": "done",
|
||||
"blocked": "blocked",
|
||||
"skipped": "skipped",
|
||||
}[status]
|
||||
|
||||
for entry_plan, _, _, idx in entries:
|
||||
if entry_plan == plan_key:
|
||||
lines[idx] = updated_line
|
||||
lines = update_workflow_state(
|
||||
lines,
|
||||
phase=workflow_phase,
|
||||
plan=f"docs/superpowers/plans/{plan_key}",
|
||||
verification=rendered_verified,
|
||||
clear_keys=verification_clear_keys,
|
||||
)
|
||||
write_progress_lines(progress_path, lines)
|
||||
return 0, updated_line
|
||||
|
||||
lines[end_idx:end_idx] = [updated_line]
|
||||
lines = update_workflow_state(
|
||||
lines,
|
||||
phase=workflow_phase,
|
||||
plan=f"docs/superpowers/plans/{plan_key}",
|
||||
verification=rendered_verified,
|
||||
clear_keys=verification_clear_keys,
|
||||
)
|
||||
write_progress_lines(progress_path, lines)
|
||||
return 0, updated_line
|
||||
|
||||
@@ -642,11 +525,6 @@ def status_report(plans_dir: Path, progress_path: Path) -> tuple[int, str]:
|
||||
suffix = f": {note}" if note else ""
|
||||
rows.append(f"PLAN {plan_key} {status}{suffix}")
|
||||
|
||||
state: dict[str, str] = {}
|
||||
workflow_block = find_named_block(lines, WORKFLOW_STATE_START, WORKFLOW_STATE_END)
|
||||
if workflow_block:
|
||||
state = parse_workflow_state(lines, *workflow_block)
|
||||
|
||||
output = [
|
||||
"STATUS "
|
||||
f"total={len(plan_keys)} "
|
||||
@@ -656,37 +534,14 @@ def status_report(plans_dir: Path, progress_path: Path) -> tuple[int, str]:
|
||||
f"blocked={counts['blocked']} "
|
||||
f"skipped={counts['skipped']}"
|
||||
]
|
||||
current_parts = [
|
||||
f"{key}={state[key]}"
|
||||
for key in (
|
||||
"phase",
|
||||
"plan",
|
||||
"claimed_by",
|
||||
"claimed_at",
|
||||
"verification",
|
||||
)
|
||||
if key in state
|
||||
]
|
||||
if current_parts:
|
||||
output.append("CURRENT " + " ".join(current_parts))
|
||||
output.extend(rows)
|
||||
return 0, "\n".join(output)
|
||||
|
||||
|
||||
def record_workflow_state(
|
||||
progress_path: Path,
|
||||
phase: str,
|
||||
spec: Optional[str],
|
||||
plan: Optional[str],
|
||||
executor: Optional[str],
|
||||
constraints: Optional[str],
|
||||
) -> tuple[int, str]:
|
||||
if phase not in WORKFLOW_PHASES:
|
||||
return 2, f"ERROR: invalid phase: {phase}"
|
||||
|
||||
def record_plan(progress_path: Path, plan: str) -> tuple[int, str]:
|
||||
with locked_progress(progress_path):
|
||||
lines = load_progress_lines(progress_path)
|
||||
lines = update_workflow_state(lines, phase, spec, plan, executor, constraints)
|
||||
lines = ensure_plan_status_entry(lines, normalize_plan_key(plan))
|
||||
write_progress_lines(progress_path, lines)
|
||||
return 0, "OK"
|
||||
|
||||
@@ -700,7 +555,7 @@ def main(argv: list[str]) -> int:
|
||||
return 0
|
||||
|
||||
mode = argv[0]
|
||||
if mode not in {"claim", "finish", "record", "status"}:
|
||||
if mode not in {"claim", "finish", "status"}:
|
||||
print(f"ERROR: unknown mode: {mode}", file=sys.stderr)
|
||||
print(usage(), file=sys.stderr)
|
||||
return 2
|
||||
@@ -744,26 +599,6 @@ def main(argv: list[str]) -> int:
|
||||
print(message)
|
||||
return 0
|
||||
|
||||
if mode == "record":
|
||||
progress = flags.get("-progress")
|
||||
phase = flags.get("-phase")
|
||||
spec = flags.get("-spec")
|
||||
plan = flags.get("-plan")
|
||||
executor = flags.get("-executor")
|
||||
constraints = flags.get("-constraints")
|
||||
if not progress or not phase:
|
||||
print("ERROR: -progress and -phase are required", file=sys.stderr)
|
||||
print(usage(), file=sys.stderr)
|
||||
return 2
|
||||
code, message = record_workflow_state(
|
||||
Path(progress), phase, spec, plan, executor, constraints
|
||||
)
|
||||
if code != 0:
|
||||
print(message, file=sys.stderr)
|
||||
return code
|
||||
print(message)
|
||||
return 0
|
||||
|
||||
plan = flags.get("-plan")
|
||||
status = flags.get("-status")
|
||||
progress = flags.get("-progress")
|
||||
|
||||
+5
-25
@@ -50,7 +50,6 @@ def usage() -> str:
|
||||
return (
|
||||
"Usage:\n"
|
||||
" python scripts/playbook.py -config <path>\n"
|
||||
" python scripts/playbook.py -record-spec <spec_path> -progress <path>\n"
|
||||
" python scripts/playbook.py -record-plan <plan_path> -progress <path>\n"
|
||||
" python scripts/playbook.py -h"
|
||||
)
|
||||
@@ -1532,23 +1531,11 @@ def main(argv: list[str]) -> int:
|
||||
|
||||
spec_path = parse_cli_value(argv, "-record-spec")
|
||||
if spec_path is not None:
|
||||
progress_path = parse_cli_value(argv, "-progress")
|
||||
if not progress_path:
|
||||
print("ERROR: -progress is required.\n" + usage(), file=sys.stderr)
|
||||
return 2
|
||||
code, message = MAIN_LOOP.record_workflow_state(
|
||||
Path(progress_path),
|
||||
"planning",
|
||||
spec_path,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
print(
|
||||
"ERROR: -record-spec has been removed; spec files are the record.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
if code != 0:
|
||||
print(message, file=sys.stderr)
|
||||
return code
|
||||
print(message)
|
||||
return 0
|
||||
return 2
|
||||
|
||||
plan_path = parse_cli_value(argv, "-record-plan")
|
||||
if plan_path is not None:
|
||||
@@ -1556,14 +1543,7 @@ def main(argv: list[str]) -> int:
|
||||
if not progress_path:
|
||||
print("ERROR: -progress is required.\n" + usage(), file=sys.stderr)
|
||||
return 2
|
||||
code, message = MAIN_LOOP.record_workflow_state(
|
||||
Path(progress_path),
|
||||
"planning",
|
||||
None,
|
||||
plan_path,
|
||||
"executing-plans",
|
||||
"karpathy-guidelines,.agents,AGENT_RULES",
|
||||
)
|
||||
code, message = MAIN_LOOP.record_plan(Path(progress_path), plan_path)
|
||||
if code != 0:
|
||||
print(message, file=sys.stderr)
|
||||
return code
|
||||
|
||||
Reference in New Issue
Block a user