🗑️ 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 Scope",
|
||||||
"Verification Gate",
|
"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: dict[str, threading.Lock] = {}
|
||||||
THREAD_LOCKS_GUARD = threading.Lock()
|
THREAD_LOCKS_GUARD = threading.Lock()
|
||||||
|
|
||||||
@@ -67,19 +49,12 @@ def usage() -> str:
|
|||||||
" python scripts/main_loop.py finish -plan <path> -status <status> "
|
" python scripts/main_loop.py finish -plan <path> -status <status> "
|
||||||
"-progress <file> [-note <text>] [-verified <text>]\n"
|
"-progress <file> [-note <text>] [-verified <text>]\n"
|
||||||
" python scripts/main_loop.py status -plans <dir> -progress <file>\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"
|
" python scripts/main_loop.py -h\n"
|
||||||
"Options:\n"
|
"Options:\n"
|
||||||
" -plans DIR\n"
|
" -plans DIR\n"
|
||||||
" -plan PATH\n"
|
" -plan PATH\n"
|
||||||
" -status done|blocked|skipped\n"
|
" -status done|blocked|skipped\n"
|
||||||
" -progress FILE\n"
|
" -progress FILE\n"
|
||||||
" -phase brainstorming|planning|executing|done|blocked|skipped\n"
|
|
||||||
" -spec PATH\n"
|
|
||||||
" -executor NAME\n"
|
|
||||||
" -constraints CSV\n"
|
|
||||||
" -note TEXT\n"
|
" -note TEXT\n"
|
||||||
" -owner NAME\n"
|
" -owner NAME\n"
|
||||||
" -verified TEXT\n"
|
" -verified TEXT\n"
|
||||||
@@ -205,11 +180,6 @@ def render_progress_lines(plans: list[str]) -> list[str]:
|
|||||||
lines = [
|
lines = [
|
||||||
"# 当前进展",
|
"# 当前进展",
|
||||||
"",
|
"",
|
||||||
"## Workflow State",
|
|
||||||
"",
|
|
||||||
WORKFLOW_STATE_START,
|
|
||||||
WORKFLOW_STATE_END,
|
|
||||||
"",
|
|
||||||
"## Plan Status",
|
"## Plan Status",
|
||||||
"",
|
"",
|
||||||
PLAN_STATUS_START,
|
PLAN_STATUS_START,
|
||||||
@@ -220,47 +190,6 @@ def render_progress_lines(plans: list[str]) -> list[str]:
|
|||||||
return lines
|
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]]:
|
def parse_env_blocked_note(note: Optional[str]) -> Optional[tuple[str, str]]:
|
||||||
if not note:
|
if not note:
|
||||||
return None
|
return None
|
||||||
@@ -360,7 +289,7 @@ def ensure_block_with_lines(
|
|||||||
def ensure_plan_block(
|
def ensure_plan_block(
|
||||||
lines: list[str], progress_path: Path, plan_keys: list[str]
|
lines: list[str], progress_path: Path, plan_keys: list[str]
|
||||||
) -> tuple[list[str], int, int]:
|
) -> 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, start_idx, end_idx = ensure_block_with_lines(
|
||||||
lines,
|
lines,
|
||||||
PLAN_STATUS_START,
|
PLAN_STATUS_START,
|
||||||
@@ -372,60 +301,26 @@ def ensure_plan_block(
|
|||||||
return lines, start_idx, end_idx
|
return lines, start_idx, end_idx
|
||||||
|
|
||||||
|
|
||||||
def ensure_workflow_state_block(
|
def remove_workflow_state_block(lines: list[str]) -> list[str]:
|
||||||
lines: list[str],
|
block = find_named_block(lines, WORKFLOW_STATE_START, WORKFLOW_STATE_END)
|
||||||
) -> tuple[list[str], int, int]:
|
if not block:
|
||||||
return ensure_block_with_lines(
|
return lines
|
||||||
lines,
|
|
||||||
WORKFLOW_STATE_START,
|
|
||||||
WORKFLOW_STATE_END,
|
|
||||||
[WORKFLOW_STATE_START, WORKFLOW_STATE_END],
|
|
||||||
"## Workflow State",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
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(
|
remove_end = end_idx + 1
|
||||||
lines: list[str],
|
if remove_end < len(lines) and lines[remove_end].strip() == "":
|
||||||
phase: Optional[str] = None,
|
remove_end += 1
|
||||||
spec: Optional[str] = None,
|
del lines[remove_start:remove_end]
|
||||||
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"),
|
|
||||||
)
|
|
||||||
return lines
|
return lines
|
||||||
|
|
||||||
|
|
||||||
@@ -450,6 +345,21 @@ def ensure_all_plans_present(
|
|||||||
return entries
|
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(
|
def filter_existing_entries(
|
||||||
entries: list[tuple[str, str, Optional[str], int]], plan_keys: list[str]
|
entries: list[tuple[str, str, Optional[str], int]], plan_keys: list[str]
|
||||||
) -> list[tuple[str, str, Optional[str], int]]:
|
) -> list[tuple[str, str, Optional[str], int]]:
|
||||||
@@ -483,12 +393,8 @@ def choose_claim_entry(
|
|||||||
current_env: Optional[str],
|
current_env: Optional[str],
|
||||||
plan_keys: list[str],
|
plan_keys: list[str],
|
||||||
) -> Optional[tuple[str, Optional[str], int]]:
|
) -> Optional[tuple[str, Optional[str], int]]:
|
||||||
entry_by_plan: dict[str, tuple[str, str, Optional[str], int]] = {}
|
available = set(plan_keys)
|
||||||
for entry in entries:
|
ordered_entries = [entry for entry in entries if entry[0] in available]
|
||||||
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
|
|
||||||
]
|
|
||||||
|
|
||||||
for plan_key, status, note, idx in ordered_entries:
|
for plan_key, status, note, idx in ordered_entries:
|
||||||
if status == "in-progress":
|
if status == "in-progress":
|
||||||
@@ -537,15 +443,12 @@ def claim_plan(
|
|||||||
return 0, "NOOP: no claimable plans"
|
return 0, "NOOP: no claimable plans"
|
||||||
|
|
||||||
plan_key, note, idx = chosen
|
plan_key, note, idx = chosen
|
||||||
lines[idx] = render_plan_line(plan_key, "in-progress", note)
|
claim_note = (
|
||||||
lines = update_workflow_state(
|
f"claimed_by: {normalize_note(owner) if owner else default_claim_owner()}; "
|
||||||
lines,
|
f"claimed_at: {now_utc()}"
|
||||||
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",),
|
|
||||||
)
|
)
|
||||||
|
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)
|
write_progress_lines(progress_path, lines)
|
||||||
|
|
||||||
output = [f"PLAN={(plans_dir / plan_key).as_posix()}"]
|
output = [f"PLAN={(plans_dir / plan_key).as_posix()}"]
|
||||||
@@ -582,7 +485,6 @@ def finish_plan(
|
|||||||
entries = parse_entries(lines, start_idx, end_idx)
|
entries = parse_entries(lines, start_idx, end_idx)
|
||||||
rendered_note = normalize_note(note) if note else None
|
rendered_note = normalize_note(note) if note else None
|
||||||
rendered_verified = normalize_note(verified) if verified else None
|
rendered_verified = normalize_note(verified) if verified else None
|
||||||
verification_clear_keys = () if rendered_verified else ("verification",)
|
|
||||||
if rendered_verified:
|
if rendered_verified:
|
||||||
verified_note = f"verified: {rendered_verified}"
|
verified_note = f"verified: {rendered_verified}"
|
||||||
rendered_note = (
|
rendered_note = (
|
||||||
@@ -591,33 +493,14 @@ def finish_plan(
|
|||||||
else verified_note
|
else verified_note
|
||||||
)
|
)
|
||||||
updated_line = render_plan_line(plan_key, status, rendered_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:
|
for entry_plan, _, _, idx in entries:
|
||||||
if entry_plan == plan_key:
|
if entry_plan == plan_key:
|
||||||
lines[idx] = updated_line
|
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)
|
write_progress_lines(progress_path, lines)
|
||||||
return 0, updated_line
|
return 0, updated_line
|
||||||
|
|
||||||
lines[end_idx:end_idx] = [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)
|
write_progress_lines(progress_path, lines)
|
||||||
return 0, updated_line
|
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 ""
|
suffix = f": {note}" if note else ""
|
||||||
rows.append(f"PLAN {plan_key} {status}{suffix}")
|
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 = [
|
output = [
|
||||||
"STATUS "
|
"STATUS "
|
||||||
f"total={len(plan_keys)} "
|
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"blocked={counts['blocked']} "
|
||||||
f"skipped={counts['skipped']}"
|
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)
|
output.extend(rows)
|
||||||
return 0, "\n".join(output)
|
return 0, "\n".join(output)
|
||||||
|
|
||||||
|
|
||||||
def record_workflow_state(
|
def record_plan(progress_path: Path, plan: str) -> tuple[int, str]:
|
||||||
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}"
|
|
||||||
|
|
||||||
with locked_progress(progress_path):
|
with locked_progress(progress_path):
|
||||||
lines = load_progress_lines(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)
|
write_progress_lines(progress_path, lines)
|
||||||
return 0, "OK"
|
return 0, "OK"
|
||||||
|
|
||||||
@@ -700,7 +555,7 @@ def main(argv: list[str]) -> int:
|
|||||||
return 0
|
return 0
|
||||||
|
|
||||||
mode = argv[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(f"ERROR: unknown mode: {mode}", file=sys.stderr)
|
||||||
print(usage(), file=sys.stderr)
|
print(usage(), file=sys.stderr)
|
||||||
return 2
|
return 2
|
||||||
@@ -744,26 +599,6 @@ def main(argv: list[str]) -> int:
|
|||||||
print(message)
|
print(message)
|
||||||
return 0
|
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")
|
plan = flags.get("-plan")
|
||||||
status = flags.get("-status")
|
status = flags.get("-status")
|
||||||
progress = flags.get("-progress")
|
progress = flags.get("-progress")
|
||||||
|
|||||||
+5
-25
@@ -50,7 +50,6 @@ def usage() -> str:
|
|||||||
return (
|
return (
|
||||||
"Usage:\n"
|
"Usage:\n"
|
||||||
" python scripts/playbook.py -config <path>\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 -record-plan <plan_path> -progress <path>\n"
|
||||||
" python scripts/playbook.py -h"
|
" python scripts/playbook.py -h"
|
||||||
)
|
)
|
||||||
@@ -1532,23 +1531,11 @@ def main(argv: list[str]) -> int:
|
|||||||
|
|
||||||
spec_path = parse_cli_value(argv, "-record-spec")
|
spec_path = parse_cli_value(argv, "-record-spec")
|
||||||
if spec_path is not None:
|
if spec_path is not None:
|
||||||
progress_path = parse_cli_value(argv, "-progress")
|
print(
|
||||||
if not progress_path:
|
"ERROR: -record-spec has been removed; spec files are the record.",
|
||||||
print("ERROR: -progress is required.\n" + usage(), file=sys.stderr)
|
file=sys.stderr,
|
||||||
return 2
|
|
||||||
code, message = MAIN_LOOP.record_workflow_state(
|
|
||||||
Path(progress_path),
|
|
||||||
"planning",
|
|
||||||
spec_path,
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
)
|
)
|
||||||
if code != 0:
|
return 2
|
||||||
print(message, file=sys.stderr)
|
|
||||||
return code
|
|
||||||
print(message)
|
|
||||||
return 0
|
|
||||||
|
|
||||||
plan_path = parse_cli_value(argv, "-record-plan")
|
plan_path = parse_cli_value(argv, "-record-plan")
|
||||||
if plan_path is not None:
|
if plan_path is not None:
|
||||||
@@ -1556,14 +1543,7 @@ def main(argv: list[str]) -> int:
|
|||||||
if not progress_path:
|
if not progress_path:
|
||||||
print("ERROR: -progress is required.\n" + usage(), file=sys.stderr)
|
print("ERROR: -progress is required.\n" + usage(), file=sys.stderr)
|
||||||
return 2
|
return 2
|
||||||
code, message = MAIN_LOOP.record_workflow_state(
|
code, message = MAIN_LOOP.record_plan(Path(progress_path), plan_path)
|
||||||
Path(progress_path),
|
|
||||||
"planning",
|
|
||||||
None,
|
|
||||||
plan_path,
|
|
||||||
"executing-plans",
|
|
||||||
"karpathy-guidelines,.agents,AGENT_RULES",
|
|
||||||
)
|
|
||||||
if code != 0:
|
if code != 0:
|
||||||
print(message, file=sys.stderr)
|
print(message, file=sys.stderr)
|
||||||
return code
|
return code
|
||||||
|
|||||||
@@ -62,12 +62,11 @@
|
|||||||
> `using-superpowers` 是会话启动时判断并加载适用 skill 的入口 skill。
|
> `using-superpowers` 是会话启动时判断并加载适用 skill 的入口 skill。
|
||||||
|
|
||||||
- 规划阶段必须走 `using-superpowers -> brainstorming -> writing-plans`
|
- 规划阶段必须走 `using-superpowers -> brainstorming -> writing-plans`
|
||||||
- `$brainstorming` 产出 `docs/superpowers/specs/*-design.md`;写出 spec 后
|
- `$brainstorming` 产出 `docs/superpowers/specs/*-design.md`;spec 文件本身即为设计留痕
|
||||||
立即用 `playbook.py -record-spec` 记录 `phase=planning` 与 `spec=<path>`
|
|
||||||
- `$writing-plans` 产出 `docs/superpowers/plans/*.md`;写出 plan 后立即用
|
- `$writing-plans` 产出 `docs/superpowers/plans/*.md`;写出 plan 后立即用
|
||||||
`playbook.py -record-plan` 记录 `plan=<path>`、`executor=executing-plans`、
|
`playbook.py -record-plan` 追加到 `memory-bank/progress.md` 的
|
||||||
`constraints=karpathy-guidelines,.agents,AGENT_RULES`
|
`plan-status` 队列,初始状态为 `pending`
|
||||||
- spec/plan 产出阶段不单独提交或归档,只做文件落地与状态留痕;如外部 skill
|
- spec/plan 产出阶段不单独提交或归档,只做文件落地与 Plan 入队;如外部 skill
|
||||||
要求写完后立即提交,以本文件为准,推迟到 Plan 完成后统一处理
|
要求写完后立即提交,以本文件为准,推迟到 Plan 完成后统一处理
|
||||||
- Plan 生命周期由 `main_loop.py` 协调,通过 `memory-bank/progress.md` 留痕
|
- Plan 生命周期由 `main_loop.py` 协调,通过 `memory-bank/progress.md` 留痕
|
||||||
- Plan 执行入口只能是主循环:领取前不得进入 `$executing-plans`,领取后默认用
|
- Plan 执行入口只能是主循环:领取前不得进入 `$executing-plans`,领取后默认用
|
||||||
@@ -118,8 +117,7 @@
|
|||||||
- `in-progress`:执行中,用于恢复中断任务
|
- `in-progress`:执行中,用于恢复中断任务
|
||||||
- `done`:已完成
|
- `done`:已完成
|
||||||
- `blocked`:阻塞,需人工介入或切换环境
|
- `blocked`:阻塞,需人工介入或切换环境
|
||||||
- `skipped`:永久跳过,不再执行,`workflow-state.phase` 也写为
|
- `skipped`:永久跳过,不再执行
|
||||||
`skipped`
|
|
||||||
|
|
||||||
`skipped` 如需恢复,必须手动改回 `pending`。
|
`skipped` 如需恢复,必须手动改回 `pending`。
|
||||||
|
|
||||||
@@ -140,7 +138,7 @@ python {{PLAYBOOK_SCRIPTS}}/main_loop.py claim \
|
|||||||
-owner "<当前session或agent标识>"
|
-owner "<当前session或agent标识>"
|
||||||
```
|
```
|
||||||
|
|
||||||
该命令在锁保护下完成:自动识别当前环境(windows/linux/darwin)、校验 Plan Meta、优先恢复 `in-progress`、选择第一个可执行 Plan、写入 `claimed_by`/`claimed_at` 并清理上一轮 `verification`。
|
该命令在锁保护下完成:自动识别当前环境(windows/linux/darwin)、校验 Plan Meta、优先恢复 `in-progress`、按 `plan-status` 行顺序选择第一个可执行 Plan,并在对应 Plan 行写入 `claimed_by`/`claimed_at`。
|
||||||
|
|
||||||
stdout 必须包含 `PLAN=<path>`;如为环境恢复,还会附带 `NOTE=env:<环境>:<Task列表>`。
|
stdout 必须包含 `PLAN=<path>`;如为环境恢复,还会附带 `NOTE=env:<环境>:<Task列表>`。
|
||||||
|
|
||||||
@@ -177,11 +175,6 @@ python {{PLAYBOOK_SCRIPTS}}/main_loop.py status \
|
|||||||
**规划留痕**:
|
**规划留痕**:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# brainstorming 完成后
|
|
||||||
python {{PLAYBOOK_SCRIPTS}}/playbook.py \
|
|
||||||
-record-spec docs/superpowers/specs/<topic>-design.md \
|
|
||||||
-progress memory-bank/progress.md
|
|
||||||
|
|
||||||
# writing-plans 完成后
|
# writing-plans 完成后
|
||||||
python {{PLAYBOOK_SCRIPTS}}/playbook.py \
|
python {{PLAYBOOK_SCRIPTS}}/playbook.py \
|
||||||
-record-plan docs/superpowers/plans/<topic>.md \
|
-record-plan docs/superpowers/plans/<topic>.md \
|
||||||
@@ -235,7 +228,7 @@ python {{PLAYBOOK_SCRIPTS}}/playbook.py \
|
|||||||
|
|
||||||
- 本轮代码、配置、测试、模板改动
|
- 本轮代码、配置、测试、模板改动
|
||||||
- 当前 Plan 文件(创建、补充、勾选 Task、记录结果等)
|
- 当前 Plan 文件(创建、补充、勾选 Task、记录结果等)
|
||||||
- `memory-bank/progress.md` 中本轮 `workflow-state`、`plan-status` 与摘要更新
|
- `memory-bank/progress.md` 中本轮 `plan-status` 与摘要更新
|
||||||
- 必要 memory 更新(如 `active-context.md`、`decisions.md`)
|
- 必要 memory 更新(如 `active-context.md`、`decisions.md`)
|
||||||
|
|
||||||
**归档约束**:
|
**归档约束**:
|
||||||
@@ -251,8 +244,9 @@ python {{PLAYBOOK_SCRIPTS}}/playbook.py \
|
|||||||
|
|
||||||
- 重要决策记录到 `memory-bank/decisions.md`
|
- 重要决策记录到 `memory-bank/decisions.md`
|
||||||
- 待确认事项在回复中显式列出
|
- 待确认事项在回复中显式列出
|
||||||
- `workflow-state` 和 `plan-status` 只能通过
|
- `plan-status` 是唯一机器状态源,只能通过
|
||||||
`{{PLAYBOOK_SCRIPTS}}/main_loop.py` 维护
|
`{{PLAYBOOK_SCRIPTS}}/main_loop.py` 或
|
||||||
|
`{{PLAYBOOK_SCRIPTS}}/playbook.py -record-plan` 维护
|
||||||
- `progress.md` 上半部分是短期状态快照,不是 changelog;
|
- `progress.md` 上半部分是短期状态快照,不是 changelog;
|
||||||
阶段变化或执行结束后整理/替换摘要,不做无限追加
|
阶段变化或执行结束后整理/替换摘要,不做无限追加
|
||||||
- `active-context.md` 是短期上下文快照,不是长期日志;
|
- `active-context.md` 是短期上下文快照,不是长期日志;
|
||||||
|
|||||||
+1
-1
@@ -109,7 +109,7 @@ templates/
|
|||||||
|
|
||||||
执行规则模板,定义 AI 的工作循环和约束。如需项目私有规则,建议维护 `AGENT_RULES.local.md`;该文件通常由 `[sync_rules]` 首次自动创建,其优先级高于 `AGENT_RULES.md`,且后续不会被 playbook 覆盖。
|
执行规则模板,定义 AI 的工作循环和约束。如需项目私有规则,建议维护 `AGENT_RULES.local.md`;该文件通常由 `[sync_rules]` 首次自动创建,其优先级高于 `AGENT_RULES.md`,且后续不会被 playbook 覆盖。
|
||||||
|
|
||||||
计划编排与执行细节统一指向 `docs/superpowers/`、`playbook.py -record-spec/-record-plan` 与 `main_loop.py claim/finish`。
|
计划编排与执行细节统一指向 `docs/superpowers/`、`playbook.py -record-plan` 与 `main_loop.py claim/finish`。
|
||||||
|
|
||||||
### AGENTS.template.md
|
### AGENTS.template.md
|
||||||
|
|
||||||
|
|||||||
@@ -6,8 +6,7 @@
|
|||||||
- 上半部分是短期状态快照,不是 changelog
|
- 上半部分是短期状态快照,不是 changelog
|
||||||
- `Recent Changes` 只保留最近 3-5 条对恢复上下文有价值的变化
|
- `Recent Changes` 只保留最近 3-5 条对恢复上下文有价值的变化
|
||||||
- 更新摘要时整理/替换旧摘要,不做无限追加
|
- 更新摘要时整理/替换旧摘要,不做无限追加
|
||||||
- 中间的 workflow-state 块记录当前阶段、spec、plan 与执行约束
|
- 下半部分的 plan-status 块由 main_loop.py 维护,是唯一机器状态源
|
||||||
- 下半部分的 plan-status 块由 main_loop.py 维护,是唯一权威状态源
|
|
||||||
-->
|
-->
|
||||||
|
|
||||||
## Current Focus
|
## Current Focus
|
||||||
@@ -31,26 +30,14 @@
|
|||||||
以下示例仅用于说明结构,真实状态由 `main_loop.py` 维护:
|
以下示例仅用于说明结构,真实状态由 `main_loop.py` 维护:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
## Workflow State
|
|
||||||
<!-- workflow-state:start -->
|
|
||||||
phase: planning
|
|
||||||
spec: docs/superpowers/specs/2026-05-18-demo-design.md
|
|
||||||
plan: docs/superpowers/plans/2026-05-18-demo.md
|
|
||||||
executor: executing-plans
|
|
||||||
constraints: karpathy-guidelines,.agents,AGENT_RULES
|
|
||||||
<!-- workflow-state:end -->
|
|
||||||
|
|
||||||
## Plan Status
|
## Plan Status
|
||||||
<!-- plan-status:start -->
|
<!-- plan-status:start -->
|
||||||
- [ ] `2026-05-18-demo.md` pending
|
- [ ] `2026-05-18-demo.md` pending
|
||||||
|
- [ ] `2026-05-19-next.md` in-progress: claimed_by: codex; claimed_at: 2026-05-19T08:00:00Z
|
||||||
|
- [x] `2026-05-20-done.md` done: verified: python -m unittest
|
||||||
<!-- plan-status:end -->
|
<!-- plan-status:end -->
|
||||||
```
|
```
|
||||||
|
|
||||||
## Workflow State
|
|
||||||
|
|
||||||
<!-- workflow-state:start -->
|
|
||||||
<!-- workflow-state:end -->
|
|
||||||
|
|
||||||
## Plan Status
|
## Plan Status
|
||||||
|
|
||||||
<!-- plan-status:start -->
|
<!-- plan-status:start -->
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
完成当前 Plan 变更归档/提交;未归档不得声明 Plan 完成
|
完成当前 Plan 变更归档/提交;未归档不得声明 Plan 完成
|
||||||
- 未验证内容必须显式说明
|
- 未验证内容必须显式说明
|
||||||
- 只写对下一轮仍重要的信息
|
- 只写对下一轮仍重要的信息
|
||||||
- 不手工改写 `workflow-state` 或 `plan-status` 状态块
|
- 不手工改写 `plan-status` 状态块
|
||||||
|
|
||||||
## 执行步骤
|
## 执行步骤
|
||||||
|
|
||||||
@@ -31,23 +31,18 @@
|
|||||||
2. 核对已运行验证与未运行验证
|
2. 核对已运行验证与未运行验证
|
||||||
3. 如本轮来自 `main_loop.py claim`,核对 `main_loop.py finish`
|
3. 如本轮来自 `main_loop.py claim`,核对 `main_loop.py finish`
|
||||||
是否已经写回 `plan-status`
|
是否已经写回 `plan-status`
|
||||||
4. 如本轮来自 `main_loop.py claim`,核对 `workflow-state.phase`
|
4. 如需回写上下文,更新 `active-context`、`progress` 上半部分和 `decisions`
|
||||||
是否与当前结果一致
|
5. 如本轮来自 `main_loop.py claim` 且结果为 `done`,按项目归档机制只归档
|
||||||
5. 如需回写上下文,更新 `active-context`、`progress` 上半部分和 `decisions`
|
|
||||||
6. 如本轮来自 `main_loop.py claim` 且结果为 `done`,按项目归档机制只归档
|
|
||||||
当前 Plan 相关差异
|
当前 Plan 相关差异
|
||||||
7. 复核剩余差异是否属于其他 session / 其他 Plan,且未混入本轮交付单元
|
6. 复核剩余差异是否属于其他 session / 其他 Plan,且未混入本轮交付单元
|
||||||
8. 输出本轮摘要与下一步
|
7. 输出本轮摘要与下一步
|
||||||
|
|
||||||
## 状态留痕复核
|
## 状态留痕复核
|
||||||
|
|
||||||
- 如本轮来自 `main_loop.py claim`,`main_loop.py finish` 是否已经写回
|
- 如本轮来自 `main_loop.py claim`,`main_loop.py finish` 是否已经写回
|
||||||
`plan-status`
|
`plan-status`
|
||||||
- 如本轮来自 `main_loop.py claim`,`workflow-state.phase` 是否与当前结果一致
|
|
||||||
- 如本轮来自 `main_loop.py claim` 且结果为 `done`,当前 Plan 相关差异
|
- 如本轮来自 `main_loop.py claim` 且结果为 `done`,当前 Plan 相关差异
|
||||||
是否已经归档/提交,或是否已说明无当前 Plan 差异
|
是否已经归档/提交,或是否已说明无当前 Plan 差异
|
||||||
- 如为代码类执行,`workflow-state` 中是否保留了
|
|
||||||
`executor=executing-plans` 与既定 `constraints`
|
|
||||||
|
|
||||||
## 输出协议
|
## 输出协议
|
||||||
|
|
||||||
|
|||||||
@@ -35,8 +35,7 @@
|
|||||||
|
|
||||||
### `memory-bank/progress.md`
|
### `memory-bank/progress.md`
|
||||||
|
|
||||||
- 先读取 `workflow-state`:当前阶段、spec、plan、executor、constraints
|
- 读取 `plan-status`:Plan 队列与机器状态
|
||||||
- 再读取 `plan-status`:当前 Plan 的机器状态
|
|
||||||
- 只更新上半部分的人类摘要,不修改状态块
|
- 只更新上半部分的人类摘要,不修改状态块
|
||||||
- 上半部分是短期状态快照,不是长期日志
|
- 上半部分是短期状态快照,不是长期日志
|
||||||
- `Recent Changes` 只保留最近 3-5 条对恢复上下文有价值的变化
|
- `Recent Changes` 只保留最近 3-5 条对恢复上下文有价值的变化
|
||||||
@@ -61,14 +60,14 @@
|
|||||||
- 临时聊天内容不要写进去
|
- 临时聊天内容不要写进去
|
||||||
- 高变化信息放 `active-context`,稳定技术模式放 `system-patterns`
|
- 高变化信息放 `active-context`,稳定技术模式放 `system-patterns`
|
||||||
- 流程规则或项目私有约束变更写入 `AGENT_RULES.local.md`
|
- 流程规则或项目私有约束变更写入 `AGENT_RULES.local.md`
|
||||||
- `progress.md` 的状态块只由 `main_loop.py` 维护
|
- `progress.md` 的 `plan-status` 状态块只由 `main_loop.py` 或
|
||||||
- 摘要应与 `workflow-state` / `plan-status` 保持一致
|
`playbook.py -record-plan` 维护
|
||||||
|
- 摘要应与 `plan-status` 保持一致
|
||||||
- 摘要区保持短期状态快照;长期历史依赖项目归档记录、Plan 文件和
|
- 摘要区保持短期状态快照;长期历史依赖项目归档记录、Plan 文件和
|
||||||
`decisions.md`
|
`decisions.md`
|
||||||
|
|
||||||
## 禁止事项
|
## 禁止事项
|
||||||
|
|
||||||
- 手工改写 `<!-- workflow-state:start/end -->`
|
|
||||||
- 手工改写 `<!-- plan-status:start/end -->`
|
- 手工改写 `<!-- plan-status:start/end -->`
|
||||||
- 把临时聊天内容、未验证猜测写进摘要
|
- 把临时聊天内容、未验证猜测写进摘要
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
- 验证命令必须 fresh run
|
- 验证命令必须 fresh run
|
||||||
- 局部修改优先局部验证
|
- 局部修改优先局部验证
|
||||||
- 不能运行的验证必须写明原因
|
- 不能运行的验证必须写明原因
|
||||||
- 不手工改写 `workflow-state` 或 `plan-status` 状态块
|
- 不手工改写 `plan-status` 状态块
|
||||||
- 如本轮来自 `main_loop.py claim`,验证通过不等于 Plan 完成;Plan
|
- 如本轮来自 `main_loop.py claim`,验证通过不等于 Plan 完成;Plan
|
||||||
`done` 还必须完成当前 Plan 变更归档/提交
|
`done` 还必须完成当前 Plan 变更归档/提交
|
||||||
|
|
||||||
@@ -33,8 +33,7 @@
|
|||||||
2. 运行与本次改动直接相关的验证命令
|
2. 运行与本次改动直接相关的验证命令
|
||||||
3. 记录命令、结果和关键输出
|
3. 记录命令、结果和关键输出
|
||||||
4. 复核 diff 是否只包含预期修改
|
4. 复核 diff 是否只包含预期修改
|
||||||
5. 如本轮来自 `main_loop.py claim`,复核 `workflow-state.phase`、
|
5. 如本轮来自 `main_loop.py claim`,复核 `plan-status` 与当前声明一致
|
||||||
`plan-status` 与当前声明一致
|
|
||||||
6. 如本轮来自 `main_loop.py claim` 且结果为 `done`,复核当前 Plan
|
6. 如本轮来自 `main_loop.py claim` 且结果为 `done`,复核当前 Plan
|
||||||
相关差异是否已经归档/提交;未归档时只能声明“验证完成”,
|
相关差异是否已经归档/提交;未归档时只能声明“验证完成”,
|
||||||
不能声明“Plan 完成”
|
不能声明“Plan 完成”
|
||||||
@@ -58,14 +57,10 @@
|
|||||||
|
|
||||||
## 状态留痕复核
|
## 状态留痕复核
|
||||||
|
|
||||||
- 如本轮来自 `main_loop.py claim`,`workflow-state.phase` 是否与当前声明一致
|
|
||||||
- 如本轮来自 `main_loop.py claim`,`plan-status` 是否已经通过
|
- 如本轮来自 `main_loop.py claim`,`plan-status` 是否已经通过
|
||||||
`main_loop.py finish` 写回
|
`main_loop.py finish` 写回
|
||||||
- 如本轮来自 `main_loop.py claim` 且结果为 `done`,当前 Plan 相关差异
|
- 如本轮来自 `main_loop.py claim` 且结果为 `done`,当前 Plan 相关差异
|
||||||
是否已经归档/提交,或是否已说明无当前 Plan 差异
|
是否已经归档/提交,或是否已说明无当前 Plan 差异
|
||||||
- 如为代码类任务,`workflow-state` 中是否保留:
|
|
||||||
`executor=executing-plans`
|
|
||||||
`constraints=karpathy-guidelines,.agents,AGENT_RULES`
|
|
||||||
|
|
||||||
## 停止条件
|
## 停止条件
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ class PlaybookCliTests(unittest.TestCase):
|
|||||||
self.assertEqual(result.returncode, 0)
|
self.assertEqual(result.returncode, 0)
|
||||||
self.assertIn("Usage:", result.stdout + result.stderr)
|
self.assertIn("Usage:", result.stdout + result.stderr)
|
||||||
|
|
||||||
def test_record_spec_updates_progress_workflow_state(self):
|
def test_record_spec_is_removed(self):
|
||||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||||
root = Path(tmp_dir)
|
root = Path(tmp_dir)
|
||||||
progress = root / "memory-bank" / "progress.md"
|
progress = root / "memory-bank" / "progress.md"
|
||||||
@@ -47,12 +47,13 @@ class PlaybookCliTests(unittest.TestCase):
|
|||||||
str(progress),
|
str(progress),
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(result.returncode, 0, msg=result.stdout + result.stderr)
|
self.assertEqual(result.returncode, 2)
|
||||||
|
self.assertIn("-record-spec has been removed", result.stderr)
|
||||||
text = progress.read_text(encoding="utf-8")
|
text = progress.read_text(encoding="utf-8")
|
||||||
self.assertIn("phase: planning", text)
|
self.assertNotIn("workflow-state", text)
|
||||||
self.assertIn("spec: docs/superpowers/specs/2026-05-18-demo-design.md", text)
|
self.assertNotIn("2026-05-18-demo-design.md", text)
|
||||||
|
|
||||||
def test_record_plan_updates_progress_workflow_state(self):
|
def test_record_plan_appends_pending_plan_status(self):
|
||||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||||
root = Path(tmp_dir)
|
root = Path(tmp_dir)
|
||||||
progress = root / "memory-bank" / "progress.md"
|
progress = root / "memory-bank" / "progress.md"
|
||||||
@@ -62,12 +63,10 @@ class PlaybookCliTests(unittest.TestCase):
|
|||||||
[
|
[
|
||||||
"# 当前进展",
|
"# 当前进展",
|
||||||
"",
|
"",
|
||||||
"## Workflow State",
|
"## Plan Status",
|
||||||
"",
|
"",
|
||||||
"<!-- workflow-state:start -->",
|
"<!-- plan-status:start -->",
|
||||||
"phase: planning",
|
"<!-- plan-status:end -->",
|
||||||
"spec: docs/superpowers/specs/2026-05-18-demo-design.md",
|
|
||||||
"<!-- workflow-state:end -->",
|
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
+ "\n",
|
+ "\n",
|
||||||
@@ -83,13 +82,44 @@ class PlaybookCliTests(unittest.TestCase):
|
|||||||
|
|
||||||
self.assertEqual(result.returncode, 0, msg=result.stdout + result.stderr)
|
self.assertEqual(result.returncode, 0, msg=result.stdout + result.stderr)
|
||||||
text = progress.read_text(encoding="utf-8")
|
text = progress.read_text(encoding="utf-8")
|
||||||
self.assertIn("phase: planning", text)
|
self.assertNotIn("workflow-state", text)
|
||||||
self.assertIn("spec: docs/superpowers/specs/2026-05-18-demo-design.md", text)
|
self.assertIn("- [ ] `2026-05-18-demo.md` pending", text)
|
||||||
self.assertIn("plan: docs/superpowers/plans/2026-05-18-demo.md", text)
|
|
||||||
self.assertIn("executor: executing-plans", text)
|
def test_record_plan_appends_pending_plan_status_after_existing_entries(self):
|
||||||
self.assertIn(
|
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||||
"constraints: karpathy-guidelines,.agents,AGENT_RULES",
|
root = Path(tmp_dir)
|
||||||
text,
|
progress = root / "memory-bank" / "progress.md"
|
||||||
|
progress.parent.mkdir(parents=True)
|
||||||
|
progress.write_text(
|
||||||
|
"\n".join(
|
||||||
|
[
|
||||||
|
"# 当前进展",
|
||||||
|
"",
|
||||||
|
"## Plan Status",
|
||||||
|
"",
|
||||||
|
"<!-- plan-status:start -->",
|
||||||
|
"- [ ] `2026-05-18-first.md` pending",
|
||||||
|
"<!-- plan-status:end -->",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
+ "\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
result = run_cli(
|
||||||
|
"-record-plan",
|
||||||
|
"docs/superpowers/plans/2026-05-18-second.md",
|
||||||
|
"-progress",
|
||||||
|
str(progress),
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(result.returncode, 0, msg=result.stdout + result.stderr)
|
||||||
|
text = progress.read_text(encoding="utf-8")
|
||||||
|
self.assertIn("- [ ] `2026-05-18-first.md` pending", text)
|
||||||
|
self.assertIn("- [ ] `2026-05-18-second.md` pending", text)
|
||||||
|
self.assertLess(
|
||||||
|
text.index("`2026-05-18-first.md`"),
|
||||||
|
text.index("`2026-05-18-second.md`"),
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_missing_config_is_error(self):
|
def test_missing_config_is_error(self):
|
||||||
|
|||||||
+113
-146
@@ -11,6 +11,7 @@ from pathlib import Path
|
|||||||
|
|
||||||
ROOT = Path(__file__).resolve().parents[1]
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
SCRIPT = ROOT / "scripts" / "main_loop.py"
|
SCRIPT = ROOT / "scripts" / "main_loop.py"
|
||||||
|
PLAYBOOK_SCRIPT = ROOT / "scripts" / "playbook.py"
|
||||||
|
|
||||||
_SPEC = importlib.util.spec_from_file_location("playbook_main_loop", SCRIPT)
|
_SPEC = importlib.util.spec_from_file_location("playbook_main_loop", SCRIPT)
|
||||||
assert _SPEC and _SPEC.loader
|
assert _SPEC and _SPEC.loader
|
||||||
@@ -82,15 +83,65 @@ class MainLoopCliTests(unittest.TestCase):
|
|||||||
|
|
||||||
progress = root / "memory-bank" / "progress.md"
|
progress = root / "memory-bank" / "progress.md"
|
||||||
text = progress.read_text(encoding="utf-8")
|
text = progress.read_text(encoding="utf-8")
|
||||||
self.assertIn("<!-- workflow-state:start -->", text)
|
self.assertNotIn("workflow-state", text)
|
||||||
self.assertIn("<!-- workflow-state:end -->", text)
|
|
||||||
self.assertIn("phase: executing", text)
|
|
||||||
self.assertIn("plan: docs/superpowers/plans/2026-01-01-old.md", text)
|
|
||||||
self.assertIn("<!-- plan-status:start -->", text)
|
self.assertIn("<!-- plan-status:start -->", text)
|
||||||
self.assertIn("<!-- plan-status:end -->", text)
|
self.assertIn("<!-- plan-status:end -->", text)
|
||||||
self.assertIn("`2026-01-01-old.md` in-progress", text)
|
self.assertIn("`2026-01-01-old.md` in-progress", text)
|
||||||
|
self.assertIn("claimed_by:", text)
|
||||||
|
self.assertIn("claimed_at:", text)
|
||||||
self.assertIn("`2026-01-02-new.md` pending", text)
|
self.assertIn("`2026-01-02-new.md` pending", text)
|
||||||
|
|
||||||
|
def test_claim_uses_plan_status_order_before_filename_order(self):
|
||||||
|
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||||
|
root = Path(tmp_dir)
|
||||||
|
plans_dir = root / "docs" / "superpowers" / "plans"
|
||||||
|
plans_dir.mkdir(parents=True)
|
||||||
|
(plans_dir / "2026-01-01-alpha.md").write_text(
|
||||||
|
valid_plan_text("alpha"), encoding="utf-8"
|
||||||
|
)
|
||||||
|
(plans_dir / "2026-01-02-beta.md").write_text(
|
||||||
|
valid_plan_text("beta"), encoding="utf-8"
|
||||||
|
)
|
||||||
|
|
||||||
|
progress = root / "memory-bank" / "progress.md"
|
||||||
|
progress.parent.mkdir(parents=True)
|
||||||
|
progress.write_text(
|
||||||
|
"\n".join(
|
||||||
|
[
|
||||||
|
"# 当前进展",
|
||||||
|
"",
|
||||||
|
"## Plan Status",
|
||||||
|
"",
|
||||||
|
"<!-- plan-status:start -->",
|
||||||
|
"- [ ] `2026-01-02-beta.md` pending",
|
||||||
|
"- [ ] `2026-01-01-alpha.md` pending",
|
||||||
|
"<!-- plan-status:end -->",
|
||||||
|
"",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
+ "\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
result = run_cli(
|
||||||
|
"claim",
|
||||||
|
"-plans",
|
||||||
|
"docs/superpowers/plans",
|
||||||
|
"-progress",
|
||||||
|
"memory-bank/progress.md",
|
||||||
|
cwd=root,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(result.returncode, 0, msg=result.stderr)
|
||||||
|
self.assertEqual(
|
||||||
|
result.stdout.strip(),
|
||||||
|
"PLAN=docs/superpowers/plans/2026-01-02-beta.md",
|
||||||
|
)
|
||||||
|
|
||||||
|
text = progress.read_text(encoding="utf-8")
|
||||||
|
self.assertIn("- [ ] `2026-01-02-beta.md` in-progress", text)
|
||||||
|
self.assertIn("- [ ] `2026-01-01-alpha.md` pending", text)
|
||||||
|
|
||||||
def test_claim_preserves_human_progress_sections_when_plan_block_missing(self):
|
def test_claim_preserves_human_progress_sections_when_plan_block_missing(self):
|
||||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||||
root = Path(tmp_dir)
|
root = Path(tmp_dir)
|
||||||
@@ -134,7 +185,7 @@ class MainLoopCliTests(unittest.TestCase):
|
|||||||
text = progress.read_text(encoding="utf-8")
|
text = progress.read_text(encoding="utf-8")
|
||||||
self.assertIn("- keep-this-focus", text)
|
self.assertIn("- keep-this-focus", text)
|
||||||
self.assertIn("- keep-this-change", text)
|
self.assertIn("- keep-this-change", text)
|
||||||
self.assertIn("<!-- workflow-state:start -->", text)
|
self.assertNotIn("workflow-state", text)
|
||||||
self.assertIn("<!-- plan-status:start -->", text)
|
self.assertIn("<!-- plan-status:start -->", text)
|
||||||
self.assertIn("`2026-01-01-demo.md` in-progress", text)
|
self.assertIn("`2026-01-01-demo.md` in-progress", text)
|
||||||
|
|
||||||
@@ -198,10 +249,6 @@ class MainLoopCliTests(unittest.TestCase):
|
|||||||
[
|
[
|
||||||
"# Plan 状态",
|
"# Plan 状态",
|
||||||
"",
|
"",
|
||||||
"<!-- workflow-state:start -->",
|
|
||||||
"phase: planning",
|
|
||||||
"<!-- workflow-state:end -->",
|
|
||||||
"",
|
|
||||||
"<!-- plan-status:start -->",
|
"<!-- plan-status:start -->",
|
||||||
"- [ ] `2026-01-01-deleted.md` pending",
|
"- [ ] `2026-01-01-deleted.md` pending",
|
||||||
"- [ ] `2026-01-02-live.md` pending",
|
"- [ ] `2026-01-02-live.md` pending",
|
||||||
@@ -351,7 +398,7 @@ class MainLoopCliTests(unittest.TestCase):
|
|||||||
self.assertIn("missing required Plan Meta", result.stderr)
|
self.assertIn("missing required Plan Meta", result.stderr)
|
||||||
self.assertIn("2026-01-01-invalid.md", result.stderr)
|
self.assertIn("2026-01-01-invalid.md", result.stderr)
|
||||||
|
|
||||||
def test_claim_records_claim_metadata_in_workflow_state(self):
|
def test_claim_records_claim_metadata_in_plan_status(self):
|
||||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||||
root = Path(tmp_dir)
|
root = Path(tmp_dir)
|
||||||
plans_dir = root / "docs" / "superpowers" / "plans"
|
plans_dir = root / "docs" / "superpowers" / "plans"
|
||||||
@@ -375,10 +422,13 @@ class MainLoopCliTests(unittest.TestCase):
|
|||||||
text = (root / "memory-bank" / "progress.md").read_text(
|
text = (root / "memory-bank" / "progress.md").read_text(
|
||||||
encoding="utf-8"
|
encoding="utf-8"
|
||||||
)
|
)
|
||||||
self.assertIn("claimed_by: codex-test", text)
|
self.assertIn(
|
||||||
|
"- [ ] `2026-01-01-demo.md` in-progress: claimed_by: codex-test; ",
|
||||||
|
text,
|
||||||
|
)
|
||||||
self.assertRegex(text, r"claimed_at: \d{4}-\d{2}-\d{2}T")
|
self.assertRegex(text, r"claimed_at: \d{4}-\d{2}-\d{2}T")
|
||||||
|
|
||||||
def test_claim_clears_stale_verification_from_workflow_state(self):
|
def test_claim_removes_legacy_workflow_state(self):
|
||||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||||
root = Path(tmp_dir)
|
root = Path(tmp_dir)
|
||||||
plans_dir = root / "docs" / "superpowers" / "plans"
|
plans_dir = root / "docs" / "superpowers" / "plans"
|
||||||
@@ -420,6 +470,7 @@ class MainLoopCliTests(unittest.TestCase):
|
|||||||
self.assertEqual(result.returncode, 0, msg=result.stderr)
|
self.assertEqual(result.returncode, 0, msg=result.stderr)
|
||||||
text = progress.read_text(encoding="utf-8")
|
text = progress.read_text(encoding="utf-8")
|
||||||
self.assertNotIn("verification: old evidence", text)
|
self.assertNotIn("verification: old evidence", text)
|
||||||
|
self.assertNotIn("workflow-state", text)
|
||||||
|
|
||||||
def test_finish_updates_line(self):
|
def test_finish_updates_line(self):
|
||||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||||
@@ -469,11 +520,6 @@ class MainLoopCliTests(unittest.TestCase):
|
|||||||
[
|
[
|
||||||
"# Plan 状态",
|
"# Plan 状态",
|
||||||
"",
|
"",
|
||||||
"<!-- workflow-state:start -->",
|
|
||||||
"phase: executing",
|
|
||||||
"plan: docs/superpowers/plans/2026-01-03-demo.md",
|
|
||||||
"<!-- workflow-state:end -->",
|
|
||||||
"",
|
|
||||||
"<!-- plan-status:start -->",
|
"<!-- plan-status:start -->",
|
||||||
"- [ ] `2026-01-03-demo.md` in-progress",
|
"- [ ] `2026-01-03-demo.md` in-progress",
|
||||||
"<!-- plan-status:end -->",
|
"<!-- plan-status:end -->",
|
||||||
@@ -503,12 +549,9 @@ class MainLoopCliTests(unittest.TestCase):
|
|||||||
"verified: python -m unittest tests.test_main_loop_cli",
|
"verified: python -m unittest tests.test_main_loop_cli",
|
||||||
text,
|
text,
|
||||||
)
|
)
|
||||||
self.assertIn(
|
self.assertNotIn("workflow-state", text)
|
||||||
"verification: python -m unittest tests.test_main_loop_cli",
|
|
||||||
text,
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_finish_without_verified_clears_stale_verification(self):
|
def test_finish_without_verified_removes_legacy_workflow_state(self):
|
||||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||||
root = Path(tmp_dir)
|
root = Path(tmp_dir)
|
||||||
progress = root / "memory-bank" / "progress.md"
|
progress = root / "memory-bank" / "progress.md"
|
||||||
@@ -549,9 +592,13 @@ class MainLoopCliTests(unittest.TestCase):
|
|||||||
self.assertEqual(result.returncode, 0, msg=result.stderr)
|
self.assertEqual(result.returncode, 0, msg=result.stderr)
|
||||||
text = progress.read_text(encoding="utf-8")
|
text = progress.read_text(encoding="utf-8")
|
||||||
self.assertNotIn("verification: old evidence", text)
|
self.assertNotIn("verification: old evidence", text)
|
||||||
self.assertIn("phase: blocked", text)
|
self.assertNotIn("workflow-state", text)
|
||||||
|
self.assertIn(
|
||||||
|
"- [ ] `2026-01-03-demo.md` blocked: needs confirmation",
|
||||||
|
text,
|
||||||
|
)
|
||||||
|
|
||||||
def test_finish_updates_workflow_phase_and_preserves_metadata(self):
|
def test_finish_removes_legacy_workflow_state_and_updates_plan_line(self):
|
||||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||||
root = Path(tmp_dir)
|
root = Path(tmp_dir)
|
||||||
progress = root / "memory-bank" / "progress.md"
|
progress = root / "memory-bank" / "progress.md"
|
||||||
@@ -596,17 +643,11 @@ class MainLoopCliTests(unittest.TestCase):
|
|||||||
|
|
||||||
self.assertEqual(result.returncode, 0, msg=result.stderr)
|
self.assertEqual(result.returncode, 0, msg=result.stderr)
|
||||||
text = progress.read_text(encoding="utf-8")
|
text = progress.read_text(encoding="utf-8")
|
||||||
self.assertIn("phase: done", text)
|
self.assertNotIn("workflow-state", text)
|
||||||
self.assertIn(
|
self.assertNotIn("2026-05-18-demo-design.md", text)
|
||||||
"spec: docs/superpowers/specs/2026-05-18-demo-design.md", text
|
self.assertIn("- [x] `2026-05-18-demo.md` done", text)
|
||||||
)
|
|
||||||
self.assertIn("executor: executing-plans", text)
|
|
||||||
self.assertIn(
|
|
||||||
"constraints: karpathy-guidelines,.agents,AGENT_RULES",
|
|
||||||
text,
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_finish_skipped_updates_workflow_phase_to_skipped(self):
|
def test_finish_skipped_updates_plan_line_only(self):
|
||||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||||
root = Path(tmp_dir)
|
root = Path(tmp_dir)
|
||||||
progress = root / "memory-bank" / "progress.md"
|
progress = root / "memory-bank" / "progress.md"
|
||||||
@@ -650,10 +691,10 @@ class MainLoopCliTests(unittest.TestCase):
|
|||||||
|
|
||||||
self.assertEqual(result.returncode, 0, msg=result.stderr)
|
self.assertEqual(result.returncode, 0, msg=result.stderr)
|
||||||
text = progress.read_text(encoding="utf-8")
|
text = progress.read_text(encoding="utf-8")
|
||||||
self.assertIn("phase: skipped", text)
|
self.assertNotIn("workflow-state", text)
|
||||||
self.assertIn("- [ ] `2026-05-18-demo.md` skipped: obsolete", text)
|
self.assertIn("- [ ] `2026-05-18-demo.md` skipped: obsolete", text)
|
||||||
|
|
||||||
def test_record_updates_workflow_state_block(self):
|
def test_record_mode_is_removed(self):
|
||||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||||
root = Path(tmp_dir)
|
root = Path(tmp_dir)
|
||||||
progress = root / "memory-bank" / "progress.md"
|
progress = root / "memory-bank" / "progress.md"
|
||||||
@@ -691,21 +732,12 @@ class MainLoopCliTests(unittest.TestCase):
|
|||||||
cwd=root,
|
cwd=root,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(result.returncode, 0, msg=result.stderr)
|
self.assertEqual(result.returncode, 2)
|
||||||
|
self.assertIn("unknown mode: record", result.stderr)
|
||||||
text = progress.read_text(encoding="utf-8")
|
text = progress.read_text(encoding="utf-8")
|
||||||
self.assertIn("<!-- workflow-state:start -->", text)
|
self.assertNotIn("workflow-state", text)
|
||||||
self.assertIn("phase: planning", text)
|
|
||||||
self.assertIn(
|
|
||||||
"spec: docs/superpowers/specs/2026-05-18-demo-design.md", text
|
|
||||||
)
|
|
||||||
self.assertIn("plan: docs/superpowers/plans/2026-05-18-demo.md", text)
|
|
||||||
self.assertIn("executor: executing-plans", text)
|
|
||||||
self.assertIn(
|
|
||||||
"constraints: karpathy-guidelines,.agents,AGENT_RULES",
|
|
||||||
text,
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_record_claim_finish_workflow_chain(self):
|
def test_record_plan_claim_finish_queue_chain(self):
|
||||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||||
root = Path(tmp_dir)
|
root = Path(tmp_dir)
|
||||||
plans_dir = root / "docs" / "superpowers" / "plans"
|
plans_dir = root / "docs" / "superpowers" / "plans"
|
||||||
@@ -717,35 +749,10 @@ class MainLoopCliTests(unittest.TestCase):
|
|||||||
progress = root / "memory-bank" / "progress.md"
|
progress = root / "memory-bank" / "progress.md"
|
||||||
progress.parent.mkdir(parents=True)
|
progress.parent.mkdir(parents=True)
|
||||||
|
|
||||||
result = run_cli(
|
code, message = MAIN_LOOP.record_plan(
|
||||||
"record",
|
progress, "docs/superpowers/plans/2026-05-18-demo.md"
|
||||||
"-progress",
|
|
||||||
"memory-bank/progress.md",
|
|
||||||
"-phase",
|
|
||||||
"planning",
|
|
||||||
"-spec",
|
|
||||||
"docs/superpowers/specs/2026-05-18-demo-design.md",
|
|
||||||
cwd=root,
|
|
||||||
)
|
)
|
||||||
self.assertEqual(result.returncode, 0, msg=result.stderr)
|
self.assertEqual(code, 0, msg=message)
|
||||||
|
|
||||||
result = run_cli(
|
|
||||||
"record",
|
|
||||||
"-progress",
|
|
||||||
"memory-bank/progress.md",
|
|
||||||
"-phase",
|
|
||||||
"planning",
|
|
||||||
"-spec",
|
|
||||||
"docs/superpowers/specs/2026-05-18-demo-design.md",
|
|
||||||
"-plan",
|
|
||||||
"docs/superpowers/plans/2026-05-18-demo.md",
|
|
||||||
"-executor",
|
|
||||||
"executing-plans",
|
|
||||||
"-constraints",
|
|
||||||
"karpathy-guidelines,.agents,AGENT_RULES",
|
|
||||||
cwd=root,
|
|
||||||
)
|
|
||||||
self.assertEqual(result.returncode, 0, msg=result.stderr)
|
|
||||||
|
|
||||||
result = run_cli(
|
result = run_cli(
|
||||||
"claim",
|
"claim",
|
||||||
@@ -770,19 +777,10 @@ class MainLoopCliTests(unittest.TestCase):
|
|||||||
self.assertEqual(result.returncode, 0, msg=result.stderr)
|
self.assertEqual(result.returncode, 0, msg=result.stderr)
|
||||||
|
|
||||||
text = progress.read_text(encoding="utf-8")
|
text = progress.read_text(encoding="utf-8")
|
||||||
self.assertIn("phase: done", text)
|
self.assertNotIn("workflow-state", text)
|
||||||
self.assertIn(
|
|
||||||
"spec: docs/superpowers/specs/2026-05-18-demo-design.md", text
|
|
||||||
)
|
|
||||||
self.assertIn("plan: docs/superpowers/plans/2026-05-18-demo.md", text)
|
|
||||||
self.assertIn("executor: executing-plans", text)
|
|
||||||
self.assertIn(
|
|
||||||
"constraints: karpathy-guidelines,.agents,AGENT_RULES",
|
|
||||||
text,
|
|
||||||
)
|
|
||||||
self.assertIn("- [x] `2026-05-18-demo.md` done", text)
|
self.assertIn("- [x] `2026-05-18-demo.md` done", text)
|
||||||
|
|
||||||
def test_status_reports_counts_and_current_workflow_state(self):
|
def test_status_reports_counts_from_plan_status(self):
|
||||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||||
root = Path(tmp_dir)
|
root = Path(tmp_dir)
|
||||||
plans_dir = root / "docs" / "superpowers" / "plans"
|
plans_dir = root / "docs" / "superpowers" / "plans"
|
||||||
@@ -836,14 +834,9 @@ class MainLoopCliTests(unittest.TestCase):
|
|||||||
"STATUS total=5 pending=1 in-progress=1 done=1 blocked=1 skipped=1",
|
"STATUS total=5 pending=1 in-progress=1 done=1 blocked=1 skipped=1",
|
||||||
result.stdout,
|
result.stdout,
|
||||||
)
|
)
|
||||||
self.assertIn(
|
self.assertNotIn("CURRENT ", result.stdout)
|
||||||
"CURRENT phase=executing "
|
|
||||||
"plan=docs/superpowers/plans/2026-01-02-b.md "
|
|
||||||
"claimed_by=codex-test",
|
|
||||||
result.stdout,
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_concurrent_record_preserves_spec_and_plan_metadata(self):
|
def test_concurrent_record_plan_preserves_queue_entries(self):
|
||||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||||
root = Path(tmp_dir)
|
root = Path(tmp_dir)
|
||||||
progress = root / "memory-bank" / "progress.md"
|
progress = root / "memory-bank" / "progress.md"
|
||||||
@@ -865,25 +858,17 @@ class MainLoopCliTests(unittest.TestCase):
|
|||||||
try:
|
try:
|
||||||
threads = [
|
threads = [
|
||||||
threading.Thread(
|
threading.Thread(
|
||||||
target=MAIN_LOOP.record_workflow_state,
|
target=MAIN_LOOP.record_plan,
|
||||||
args=(
|
args=(
|
||||||
progress,
|
progress,
|
||||||
"planning",
|
"docs/superpowers/plans/2026-05-18-first.md",
|
||||||
"docs/superpowers/specs/2026-05-18-demo-design.md",
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
threading.Thread(
|
threading.Thread(
|
||||||
target=MAIN_LOOP.record_workflow_state,
|
target=MAIN_LOOP.record_plan,
|
||||||
args=(
|
args=(
|
||||||
progress,
|
progress,
|
||||||
"planning",
|
"docs/superpowers/plans/2026-05-18-second.md",
|
||||||
None,
|
|
||||||
"docs/superpowers/plans/2026-05-18-demo.md",
|
|
||||||
"executing-plans",
|
|
||||||
"karpathy-guidelines,.agents,AGENT_RULES",
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
@@ -896,18 +881,11 @@ class MainLoopCliTests(unittest.TestCase):
|
|||||||
MAIN_LOOP.load_progress_lines = original_load
|
MAIN_LOOP.load_progress_lines = original_load
|
||||||
|
|
||||||
text = progress.read_text(encoding="utf-8")
|
text = progress.read_text(encoding="utf-8")
|
||||||
self.assertIn("phase: planning", text)
|
self.assertNotIn("workflow-state", text)
|
||||||
self.assertIn(
|
self.assertIn("- [ ] `2026-05-18-first.md` pending", text)
|
||||||
"spec: docs/superpowers/specs/2026-05-18-demo-design.md", text
|
self.assertIn("- [ ] `2026-05-18-second.md` pending", text)
|
||||||
)
|
|
||||||
self.assertIn("plan: docs/superpowers/plans/2026-05-18-demo.md", text)
|
|
||||||
self.assertIn("executor: executing-plans", text)
|
|
||||||
self.assertIn(
|
|
||||||
"constraints: karpathy-guidelines,.agents,AGENT_RULES",
|
|
||||||
text,
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_cross_process_record_lock_preserves_spec_and_plan_metadata(self):
|
def test_cross_process_record_plan_lock_preserves_queue_entries(self):
|
||||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||||
root = Path(tmp_dir)
|
root = Path(tmp_dir)
|
||||||
progress = root / "memory-bank" / "progress.md"
|
progress = root / "memory-bank" / "progress.md"
|
||||||
@@ -919,14 +897,11 @@ class MainLoopCliTests(unittest.TestCase):
|
|||||||
proc = subprocess.Popen(
|
proc = subprocess.Popen(
|
||||||
[
|
[
|
||||||
sys.executable,
|
sys.executable,
|
||||||
str(SCRIPT),
|
str(PLAYBOOK_SCRIPT),
|
||||||
"record",
|
"-record-plan",
|
||||||
|
"docs/superpowers/plans/2026-05-18-first.md",
|
||||||
"-progress",
|
"-progress",
|
||||||
"memory-bank/progress.md",
|
str(progress),
|
||||||
"-phase",
|
|
||||||
"planning",
|
|
||||||
"-spec",
|
|
||||||
"docs/superpowers/specs/2026-05-18-demo-design.md",
|
|
||||||
],
|
],
|
||||||
cwd=root,
|
cwd=root,
|
||||||
env=slow_env,
|
env=slow_env,
|
||||||
@@ -936,18 +911,17 @@ class MainLoopCliTests(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
time.sleep(0.05)
|
time.sleep(0.05)
|
||||||
result = run_cli(
|
result = subprocess.run(
|
||||||
"record",
|
[
|
||||||
|
sys.executable,
|
||||||
|
str(PLAYBOOK_SCRIPT),
|
||||||
|
"-record-plan",
|
||||||
|
"docs/superpowers/plans/2026-05-18-second.md",
|
||||||
"-progress",
|
"-progress",
|
||||||
"memory-bank/progress.md",
|
str(progress),
|
||||||
"-phase",
|
],
|
||||||
"planning",
|
capture_output=True,
|
||||||
"-plan",
|
text=True,
|
||||||
"docs/superpowers/plans/2026-05-18-demo.md",
|
|
||||||
"-executor",
|
|
||||||
"executing-plans",
|
|
||||||
"-constraints",
|
|
||||||
"karpathy-guidelines,.agents,AGENT_RULES",
|
|
||||||
cwd=root,
|
cwd=root,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -956,16 +930,9 @@ class MainLoopCliTests(unittest.TestCase):
|
|||||||
self.assertEqual(result.returncode, 0, msg=result.stderr)
|
self.assertEqual(result.returncode, 0, msg=result.stderr)
|
||||||
|
|
||||||
text = progress.read_text(encoding="utf-8")
|
text = progress.read_text(encoding="utf-8")
|
||||||
self.assertIn("phase: planning", text)
|
self.assertNotIn("workflow-state", text)
|
||||||
self.assertIn(
|
self.assertIn("- [ ] `2026-05-18-first.md` pending", text)
|
||||||
"spec: docs/superpowers/specs/2026-05-18-demo-design.md", text
|
self.assertIn("- [ ] `2026-05-18-second.md` pending", text)
|
||||||
)
|
|
||||||
self.assertIn("plan: docs/superpowers/plans/2026-05-18-demo.md", text)
|
|
||||||
self.assertIn("executor: executing-plans", text)
|
|
||||||
self.assertIn(
|
|
||||||
"constraints: karpathy-guidelines,.agents,AGENT_RULES",
|
|
||||||
text,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ class SyncTemplatesPlaceholdersTests(unittest.TestCase):
|
|||||||
update_memory_template = (
|
update_memory_template = (
|
||||||
ROOT / "templates" / "prompts" / "coding" / "update-memory.template.md"
|
ROOT / "templates" / "prompts" / "coding" / "update-memory.template.md"
|
||||||
).read_text(encoding="utf-8")
|
).read_text(encoding="utf-8")
|
||||||
self.assertIn("workflow-state", update_memory_template)
|
self.assertNotIn("workflow-state", update_memory_template)
|
||||||
self.assertIn("plan-status", update_memory_template)
|
self.assertIn("plan-status", update_memory_template)
|
||||||
self.assertIn("## Updated Files", update_memory_template)
|
self.assertIn("## Updated Files", update_memory_template)
|
||||||
self.assertIn("## New Context", update_memory_template)
|
self.assertIn("## New Context", update_memory_template)
|
||||||
@@ -106,7 +106,7 @@ class SyncTemplatesPlaceholdersTests(unittest.TestCase):
|
|||||||
).read_text(encoding="utf-8")
|
).read_text(encoding="utf-8")
|
||||||
self.assertIn("main_loop.py finish", close_task_template)
|
self.assertIn("main_loop.py finish", close_task_template)
|
||||||
self.assertIn("如本轮来自 `main_loop.py claim`", close_task_template)
|
self.assertIn("如本轮来自 `main_loop.py claim`", close_task_template)
|
||||||
self.assertIn("workflow-state.phase", close_task_template)
|
self.assertNotIn("workflow-state", close_task_template)
|
||||||
self.assertIn("未归档不得声明 Plan 完成", close_task_template)
|
self.assertIn("未归档不得声明 Plan 完成", close_task_template)
|
||||||
self.assertIn("按项目归档机制只归档", close_task_template)
|
self.assertIn("按项目归档机制只归档", close_task_template)
|
||||||
self.assertIn("状态已写回,交付未完成", close_task_template)
|
self.assertIn("状态已写回,交付未完成", close_task_template)
|
||||||
@@ -120,7 +120,7 @@ class SyncTemplatesPlaceholdersTests(unittest.TestCase):
|
|||||||
ROOT / "templates" / "prompts" / "coding" / "verify-change.template.md"
|
ROOT / "templates" / "prompts" / "coding" / "verify-change.template.md"
|
||||||
).read_text(encoding="utf-8")
|
).read_text(encoding="utf-8")
|
||||||
self.assertIn("如本轮来自 `main_loop.py claim`", verify_change_template)
|
self.assertIn("如本轮来自 `main_loop.py claim`", verify_change_template)
|
||||||
self.assertIn("workflow-state.phase", verify_change_template)
|
self.assertNotIn("workflow-state", verify_change_template)
|
||||||
self.assertIn("plan-status", verify_change_template)
|
self.assertIn("plan-status", verify_change_template)
|
||||||
self.assertIn("验证通过不等于 Plan 完成", verify_change_template)
|
self.assertIn("验证通过不等于 Plan 完成", verify_change_template)
|
||||||
self.assertIn(
|
self.assertIn(
|
||||||
@@ -269,9 +269,9 @@ langs = [\"cpp\", \"tsl\"]
|
|||||||
)
|
)
|
||||||
self.assertIn("docs/superpowers/plans", rules_text)
|
self.assertIn("docs/superpowers/plans", rules_text)
|
||||||
self.assertNotIn("plan_progress.py", rules_text)
|
self.assertNotIn("plan_progress.py", rules_text)
|
||||||
self.assertIn("记录 `phase=planning` 与 `spec=<path>`", rules_text)
|
self.assertNotIn("record-spec", rules_text)
|
||||||
self.assertIn(
|
self.assertIn(
|
||||||
"记录 `plan=<path>`、`executor=executing-plans`、",
|
"追加到 `memory-bank/progress.md` 的",
|
||||||
rules_text,
|
rules_text,
|
||||||
)
|
)
|
||||||
self.assertIn("领取前不得进入 `$executing-plans`", rules_text)
|
self.assertIn("领取前不得进入 `$executing-plans`", rules_text)
|
||||||
@@ -356,10 +356,9 @@ project_name = "MyProject"
|
|||||||
progress_text = progress.read_text(encoding="utf-8")
|
progress_text = progress.read_text(encoding="utf-8")
|
||||||
self.assertIn("## Current Focus", progress_text)
|
self.assertIn("## Current Focus", progress_text)
|
||||||
self.assertIn("## 状态块示例", progress_text)
|
self.assertIn("## 状态块示例", progress_text)
|
||||||
self.assertIn("phase: planning", progress_text)
|
self.assertNotIn("phase: planning", progress_text)
|
||||||
self.assertIn("executor: executing-plans", progress_text)
|
self.assertNotIn("executor: executing-plans", progress_text)
|
||||||
self.assertIn("<!-- workflow-state:start -->", progress_text)
|
self.assertNotIn("workflow-state", progress_text)
|
||||||
self.assertIn("<!-- workflow-state:end -->", progress_text)
|
|
||||||
self.assertIn("## Plan Status", progress_text)
|
self.assertIn("## Plan Status", progress_text)
|
||||||
self.assertIn("<!-- plan-status:start -->", progress_text)
|
self.assertIn("<!-- plan-status:start -->", progress_text)
|
||||||
self.assertIn("<!-- plan-status:end -->", progress_text)
|
self.assertIn("<!-- plan-status:end -->", progress_text)
|
||||||
|
|||||||
Reference in New Issue
Block a user