🗑️ 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:
csh
2026-06-26 17:10:09 +08:00
parent c35aba5139
commit 1911d2dda3
11 changed files with 243 additions and 462 deletions
+44 -209
View File
@@ -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
View File
@@ -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