471 lines
24 KiB
Python
471 lines
24 KiB
Python
#!/usr/bin/env python3
|
||
"""Advance the Web task ledger to the next executable leaf task.
|
||
|
||
The command is deliberately deterministic and single-shot: it never marks a
|
||
task complete. It continues an existing ``in_progress`` task, or, when that
|
||
task is already terminal, claims the first dependency-ready leaf and rebuilds
|
||
the isolated context. An agent/external scheduler can call it after each
|
||
implementation batch without asking for user confirmation.
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import argparse
|
||
import hashlib
|
||
import json
|
||
import os
|
||
import subprocess
|
||
import sys
|
||
from datetime import datetime, timezone
|
||
from pathlib import Path
|
||
|
||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||
from web_context import load_cards, predecessor_ids # noqa: E402
|
||
from implementation_first import capability_source_fingerprint, implementation_first_enabled, load_registry, user_validation_deferred # noqa: E402
|
||
from scheduler_progress import batch_retry_details, read as read_progress # noqa: E402
|
||
from failure_reconciliation import pending_keys, pending_business_repair # noqa: E402
|
||
from prepare_feature_capability_batch import stalled_batch_limit # noqa: E402
|
||
|
||
|
||
ROOT = Path(__file__).resolve().parents[1]
|
||
STATE = ROOT / "docs/current/task-state.json"
|
||
CHECKPOINT = ROOT / "docs/current/continuation-state.json"
|
||
CONTEXT_DIR = ROOT / "docs/current/context"
|
||
CANONICAL_MANIFESTS = (
|
||
ROOT / "docs/p6/python-api-manifest.json",
|
||
ROOT / "docs/p6/object-contract-matrix.json",
|
||
ROOT / "docs/p6/workbench-command-matrix.json",
|
||
)
|
||
MODULE_PIPELINE = ROOT / "docs/current/module-batch-pipeline.json"
|
||
CAPABILITY_REGISTRY = ROOT / "docs/current/feature-capability-registry.json"
|
||
PROBE_ROOT = ROOT / "freecad-source/src/WebAssembly/FreeCADCoreProbe"
|
||
STALLED_BATCH_SIZE = max(1, int(os.environ.get("FREECAD_STALLED_FEATURE_BATCH_SIZE", "4")))
|
||
STALLED_BATCH_SECONDS = max(30.0, float(os.environ.get("FREECAD_STALLED_FEATURE_BATCH_SECONDS", "180")))
|
||
MIXED_FEATURE_BATCH_SIZE = max(2, int(os.environ.get("FREECAD_MIXED_FEATURE_BATCH_SIZE", "8")))
|
||
|
||
|
||
def run(*args: str) -> None:
|
||
subprocess.run([sys.executable, *args], cwd=ROOT, check=True)
|
||
|
||
|
||
def refresh_feature_registry_if_sources_changed() -> bool:
|
||
"""Refresh the cheap capability projection only after probe source edits.
|
||
|
||
Feature batches without an outer build target still need their static
|
||
routed/required projection refreshed. Comparing mtimes avoids rescanning
|
||
probe contents on every cycle while ensuring a just-edited native adapter
|
||
is visible to the next planner pass.
|
||
"""
|
||
if not implementation_first_enabled():
|
||
return False
|
||
try:
|
||
registry_mtime = CAPABILITY_REGISTRY.stat().st_mtime_ns
|
||
except OSError:
|
||
registry_mtime = 0
|
||
source_files = list(PROBE_ROOT.glob("*Probe.cpp")) + list(PROBE_ROOT.glob("*DesktopCommands.cpp"))
|
||
if not source_files:
|
||
return False
|
||
try:
|
||
changed = any(path.stat().st_mtime_ns > registry_mtime for path in source_files)
|
||
except OSError:
|
||
return False
|
||
if not changed:
|
||
return False
|
||
result = subprocess.run(
|
||
[sys.executable, "tools/generate_feature_capability_registry.py"],
|
||
cwd=ROOT,
|
||
text=True,
|
||
capture_output=True,
|
||
check=False,
|
||
)
|
||
if result.returncode != 0:
|
||
# Keep the existing projection on a transient source read error; the
|
||
# normal scheduler error path will record the next actionable failure.
|
||
return False
|
||
if result.stdout:
|
||
print(f"FEATURE_REGISTRY_REFRESH: {result.stdout.strip()}")
|
||
return True
|
||
|
||
|
||
def selected_routed_ids(batch: dict) -> set[str]:
|
||
"""Return currently routed IDs intersecting the selected feature batch."""
|
||
if batch.get("strategy") != "feature-first-deferred-validation":
|
||
return set()
|
||
try:
|
||
payload = json.loads(CAPABILITY_REGISTRY.read_text(encoding="utf-8"))
|
||
except (OSError, json.JSONDecodeError):
|
||
return set()
|
||
routed = {
|
||
str(row.get("id"))
|
||
for row in payload.get("records", [])
|
||
if isinstance(row, dict) and row.get("implementationStatus") == "routed"
|
||
}
|
||
return {
|
||
str(row[0])
|
||
for row in batch.get("records", [])
|
||
if isinstance(row, list) and row and str(row[0]) in routed
|
||
}
|
||
|
||
|
||
def planner_script() -> str:
|
||
return "tools/prepare_implementation_business_batch.py" if implementation_first_enabled() else "tools/prepare_full_parity_module_batch.py"
|
||
|
||
|
||
def run_continuation_check() -> bool:
|
||
"""Validate the checkpoint and report whether a repair changed projections."""
|
||
try:
|
||
run("tools/check_continuation_state.py")
|
||
return False
|
||
except subprocess.CalledProcessError:
|
||
subprocess.run([sys.executable, "tools/compact_continuation_docs.py"], cwd=ROOT, check=False)
|
||
# Compression changes handoff/card hashes; regenerate the active packet
|
||
# before retrying validation so the failure loop cannot use stale data.
|
||
subprocess.run([sys.executable, "tools/prepare_web_task_context.py"], cwd=ROOT, check=False)
|
||
retry_check = subprocess.run(
|
||
[sys.executable, "tools/check_continuation_state.py"], cwd=ROOT, check=False
|
||
)
|
||
if retry_check.returncode == 0:
|
||
return False
|
||
repair = subprocess.run(
|
||
[sys.executable, "tools/repair_continuation_checkpoint.py"],
|
||
cwd=ROOT,
|
||
text=True,
|
||
capture_output=True,
|
||
check=False,
|
||
)
|
||
if repair.returncode != 0:
|
||
raise
|
||
print(repair.stdout, end="")
|
||
subprocess.run([sys.executable, "tools/prepare_web_task_context.py"], cwd=ROOT, check=False)
|
||
run("tools/check_continuation_state.py")
|
||
return True
|
||
|
||
|
||
def feature_batch_replan_size(batch: dict) -> int | None:
|
||
if not implementation_first_enabled():
|
||
return None
|
||
selected = batch.get("selected") if isinstance(batch.get("selected"), dict) else {}
|
||
selected_count = int(selected.get("recordCount", 0) or 0)
|
||
if selected.get("family") == "desktop-command-mixed" and selected_count > MIXED_FEATURE_BATCH_SIZE:
|
||
return MIXED_FEATURE_BATCH_SIZE
|
||
if selected_count <= STALLED_BATCH_SIZE:
|
||
return None
|
||
try:
|
||
pipeline = json.loads(MODULE_PIPELINE.read_text(encoding="utf-8"))
|
||
except (OSError, json.JSONDecodeError):
|
||
pipeline = {}
|
||
return stalled_batch_limit(batch, pipeline)
|
||
|
||
|
||
def batch_requires_refresh() -> bool:
|
||
"""Return true for a stale/undersized projection before validation runs."""
|
||
batch_path = ROOT / "docs/current/module-batch-manifest.json"
|
||
if not batch_path.exists():
|
||
return True
|
||
try:
|
||
batch = json.loads(batch_path.read_text(encoding="utf-8"))
|
||
checkpoint = json.loads(CHECKPOINT.read_text(encoding="utf-8"))
|
||
task_state = json.loads(STATE.read_text(encoding="utf-8"))
|
||
selected = batch.get("selected") if isinstance(batch.get("selected"), dict) else {}
|
||
minimum = int(checkpoint.get("batchPolicy", {}).get("minRecords", 2))
|
||
completed_batches = checkpoint.get("latestBatch", {}).get("completed", [])
|
||
batch_checkpoint = checkpoint.get("batch_checkpoint") if isinstance(checkpoint.get("batch_checkpoint"), dict) else {}
|
||
if not batch_checkpoint and isinstance(task_state.get("batch_checkpoint"), dict):
|
||
batch_checkpoint = task_state["batch_checkpoint"]
|
||
batch_id = batch.get("batchId")
|
||
if user_validation_deferred() and selected.get("tier") in {"business-acceptance", "failure-reconciliation"}:
|
||
return True
|
||
if implementation_first_enabled() and batch_retry_details(batch):
|
||
return True
|
||
if implementation_first_enabled() and selected.get("tier") == "feature-workflow":
|
||
from prepare_implementation_business_batch import acceptance_preempts
|
||
if acceptance_preempts(batch):
|
||
return True
|
||
if implementation_first_enabled() and selected.get("tier") == "failure-reconciliation":
|
||
remaining = pending_keys(batch.get("failureKeys", []))
|
||
return (batch.get("status") != "ready" or not batch.get("failureKeys")
|
||
or len(remaining) != len(batch["failureKeys"]) or pending_business_repair(batch["failureKeys"]))
|
||
if implementation_first_enabled() and selected.get("tier") == "business-acceptance":
|
||
from prepare_business_acceptance_batch import inventory
|
||
pending = {row["id"] for row in inventory()["pending"]}
|
||
ids = {str(row[0]) for row in batch.get("records", []) if row}
|
||
if batch.get("status") != "ready" or not ids or not ids <= pending:
|
||
return True
|
||
generated = datetime.fromisoformat(str(batch.get("generatedAt", "")).replace("Z", "+00:00")).timestamp()
|
||
history = read_progress(ROOT / "docs/current/scheduler-progress.json").get("history", [])
|
||
return any(row.get("batchId") == batch_id and row.get("tier") == "business-acceptance"
|
||
and row.get("outcome") != "provider_wait" and float(row.get("at", 0)) >= generated for row in history)
|
||
if implementation_first_enabled() and batch.get("strategy") != "feature-first-deferred-validation":
|
||
return True
|
||
if implementation_first_enabled() and batch.get("status") == "no_ready_batch":
|
||
return batch.get("sourceFingerprint") != capability_source_fingerprint()
|
||
# A feature batch can become partially routed from native source edits
|
||
# even when no outer build target exists. Replan immediately so the
|
||
# next turn receives only the remaining records instead of rereading
|
||
# already implemented commands.
|
||
if implementation_first_enabled() and selected_routed_ids(batch):
|
||
return True
|
||
if feature_batch_replan_size(batch) is not None:
|
||
return True
|
||
implementation_batches = load_registry().get("batches", {}) if implementation_first_enabled() else {}
|
||
if isinstance(implementation_batches, dict) and batch_id in implementation_batches:
|
||
return True
|
||
if batch_records_have_evidence(batch):
|
||
return True
|
||
if batch_id in completed_batches or (
|
||
batch_checkpoint.get("batchId") == batch_id
|
||
and str(batch_checkpoint.get("status", "")) in {"pass", "pass_with_boundaries", "completed", "blocked_native_probe"}
|
||
):
|
||
return True
|
||
if batch.get("status") != "ready" or batch.get("taskId") not in (None, checkpoint.get("activeTask")):
|
||
return True
|
||
return int(selected.get("recordCount", 0)) < minimum and batch.get("singletonFallback") is not True
|
||
except (OSError, json.JSONDecodeError, TypeError, ValueError):
|
||
return True
|
||
|
||
|
||
def batch_records_have_evidence(batch: dict) -> bool:
|
||
"""Detect a completed batch even when its compact checkpoint was interrupted."""
|
||
if implementation_first_enabled():
|
||
# Current business completion is owned by implementation receipts and
|
||
# business acceptance, not the legacy API/object evidence inventories.
|
||
return False
|
||
records = batch.get("records") if isinstance(batch.get("records"), list) else []
|
||
ids = {row[0] for row in records if isinstance(row, list) and row}
|
||
if not ids:
|
||
return False
|
||
found: dict[str, dict] = {}
|
||
batch_id = batch.get("batchId")
|
||
if isinstance(batch_id, str):
|
||
for evidence_path in (ROOT / "docs/p6").glob(f"*{batch_id}*runtime.json"):
|
||
try:
|
||
evidence = json.loads(evidence_path.read_text(encoding="utf-8"))
|
||
except (OSError, json.JSONDecodeError):
|
||
continue
|
||
evidence_state = str(evidence.get("status") or (evidence.get("result") if isinstance(evidence.get("result"), str) else "") or "")
|
||
if not (evidence_state in {"pass", "passed", "pass_with_boundaries", "completed", "blocked_native_probe", "pass_with_restricted_and_pending"} or evidence_state.startswith("pass_with_")):
|
||
continue
|
||
result_payload = evidence.get("result") if isinstance(evidence.get("result"), dict) else {}
|
||
evidence_rows = evidence.get("records", []) if isinstance(evidence.get("records"), list) else result_payload.get("records", [])
|
||
for row in evidence_rows if isinstance(evidence_rows, list) else []:
|
||
if isinstance(row, dict):
|
||
record_id = row.get("apiId") or row.get("id")
|
||
if isinstance(record_id, str) and record_id in ids:
|
||
found[record_id] = row
|
||
if len(found) == len(ids):
|
||
return True
|
||
for path in CANONICAL_MANIFESTS:
|
||
try:
|
||
payload = json.loads(path.read_text(encoding="utf-8"))
|
||
except (OSError, json.JSONDecodeError):
|
||
continue
|
||
rows = payload.get("records", []) if isinstance(payload, dict) else []
|
||
for row in rows:
|
||
if isinstance(row, dict) and row.get("id") in ids:
|
||
found[str(row["id"])] = row
|
||
if len(found) != len(ids):
|
||
return False
|
||
return all(isinstance(row.get("dispositionEvidence"), list) and bool(row["dispositionEvidence"]) for row in found.values())
|
||
|
||
|
||
def context_requires_refresh(active: str) -> bool:
|
||
"""Use the materialized manifest as an incremental context fingerprint."""
|
||
manifest_path = CONTEXT_DIR / f"{active}.json"
|
||
try:
|
||
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
||
if manifest.get("task_id") != active or manifest.get("active_task") != active:
|
||
return True
|
||
for entry in manifest.get("files", []):
|
||
if entry.get("mode") != "read":
|
||
continue
|
||
path = ROOT / str(entry.get("path", ""))
|
||
if not path.is_file():
|
||
return True
|
||
stat = path.stat()
|
||
# New packets carry size/mtime metadata, so the common unchanged
|
||
# path avoids rereading every context file just to hash it. Keep a
|
||
# hash fallback for older manifests and unusual timestamp changes.
|
||
recorded_size = entry.get("bytes")
|
||
recorded_mtime = entry.get("mtime_ns")
|
||
if recorded_size is None or recorded_mtime is None:
|
||
# Upgrade legacy packets once so subsequent cycles can use the
|
||
# cheap metadata path.
|
||
return True
|
||
if stat.st_size == int(recorded_size) and stat.st_mtime_ns == int(recorded_mtime):
|
||
continue
|
||
if hashlib.sha256(path.read_bytes()).hexdigest() != entry.get("sha256"):
|
||
return True
|
||
return False
|
||
except (OSError, json.JSONDecodeError, TypeError):
|
||
return True
|
||
def prepare_module_batch() -> None:
|
||
batch_path = ROOT / "docs/current/module-batch-manifest.json"
|
||
if batch_path.exists():
|
||
try:
|
||
batch = json.loads(batch_path.read_text(encoding="utf-8"))
|
||
checkpoint = json.loads(CHECKPOINT.read_text(encoding="utf-8"))
|
||
task_state = json.loads(STATE.read_text(encoding="utf-8"))
|
||
selected = batch.get("selected") if isinstance(batch.get("selected"), dict) else {}
|
||
minimum = int(checkpoint.get("batchPolicy", {}).get("minRecords", 2))
|
||
completed_batches = checkpoint.get("latestBatch", {}).get("completed", [])
|
||
implementation_batches = load_registry().get("batches", {}) if implementation_first_enabled() else {}
|
||
batch_checkpoint = checkpoint.get("batch_checkpoint") if isinstance(checkpoint.get("batch_checkpoint"), dict) else {}
|
||
if not batch_checkpoint and isinstance(task_state.get("batch_checkpoint"), dict):
|
||
batch_checkpoint = task_state["batch_checkpoint"]
|
||
# A completed implementation batch may have explicitly selected
|
||
# the next family. Preserve that selection across CONTINUE turns;
|
||
# the planner's default ordering would otherwise replay the same
|
||
# unresolved boundary batch indefinitely.
|
||
if (
|
||
batch.get("status") == "ready"
|
||
and not (user_validation_deferred() and selected.get("tier") in {"business-acceptance", "failure-reconciliation"})
|
||
and (not implementation_first_enabled() or batch.get("strategy") == "feature-first-deferred-validation"
|
||
or selected.get("tier") == "failure-reconciliation" and bool(batch.get("failureKeys"))
|
||
and len(pending_keys(batch["failureKeys"])) == len(batch["failureKeys"])
|
||
and not pending_business_repair(batch.get("failureKeys", [])))
|
||
and not batch_retry_details(batch)
|
||
and batch.get("batchId")
|
||
and batch.get("batchId") == checkpoint.get("batchId")
|
||
and batch.get("batchId") not in completed_batches
|
||
and not (isinstance(implementation_batches, dict) and batch.get("batchId") in implementation_batches)
|
||
and not batch_records_have_evidence(batch)
|
||
and not selected_routed_ids(batch)
|
||
and feature_batch_replan_size(batch) is None
|
||
and not (
|
||
batch_checkpoint.get("batchId") == batch.get("batchId")
|
||
and str(batch_checkpoint.get("status", "")) in {"pass", "pass_with_boundaries", "completed", "blocked_native_probe"}
|
||
)
|
||
and (int(selected.get("recordCount", 0)) >= minimum or batch.get("singletonFallback") is True)
|
||
):
|
||
print(
|
||
f"MODULE_BATCH_PRESERVED: batchId={batch.get('batchId')} "
|
||
f"module={selected.get('module')} tier={selected.get('tier')} "
|
||
f"family={selected.get('family')} records={selected.get('recordCount')}"
|
||
)
|
||
return
|
||
except (OSError, json.JSONDecodeError):
|
||
pass
|
||
result = subprocess.run(
|
||
[sys.executable, planner_script()],
|
||
cwd=ROOT,
|
||
text=True,
|
||
capture_output=True,
|
||
check=False,
|
||
)
|
||
if result.stdout:
|
||
print(result.stdout, end="")
|
||
if result.returncode not in (0, 2):
|
||
raise subprocess.CalledProcessError(result.returncode, result.args, result.stdout, result.stderr)
|
||
if batch_path.exists():
|
||
try:
|
||
batch = json.loads(batch_path.read_text(encoding="utf-8"))
|
||
checkpoint = json.loads(CHECKPOINT.read_text(encoding="utf-8"))
|
||
checkpoint["batchManifest"] = "docs/current/module-batch-manifest.json"
|
||
checkpoint["batchId"] = batch.get("batchId")
|
||
checkpoint["batchSelection"] = batch.get("selected")
|
||
selected = batch.get("selected") if isinstance(batch.get("selected"), dict) else {}
|
||
if selected:
|
||
checkpoint["nextAction"] = (
|
||
f"继续执行 Part batch {batch.get('batchId')} 的 "
|
||
f"{selected.get('module')}/{selected.get('tier')}/{selected.get('family')} "
|
||
f"{selected.get('recordCount')} 条记录;复用统一 native dispatcher、Facade 和 Worker,"
|
||
"仅以真实 FreeCAD/WASM evidence 晋级,其余保留显式边界。"
|
||
)
|
||
CHECKPOINT.write_text(json.dumps(checkpoint, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||
except (OSError, json.JSONDecodeError):
|
||
pass
|
||
|
||
|
||
def update_checkpoint(active: str, card_text: str) -> None:
|
||
checkpoint = json.loads(CHECKPOINT.read_text(encoding="utf-8"))
|
||
checkpoint["activeTask"] = active
|
||
checkpoint["phase"] = "CONTINUE"
|
||
checkpoint["lastCheckpoint"] = "CONTINUE"
|
||
checkpoint["lastTransition"] = datetime.now(timezone.utc).isoformat()
|
||
contract = next((line.split("合同:", 1)[1].strip() for line in card_text.splitlines() if "合同:" in line), "执行当前任务卡中的合同")
|
||
checkpoint["nextAction"] = contract
|
||
CHECKPOINT.write_text(json.dumps(checkpoint, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||
|
||
|
||
def main() -> int:
|
||
parser = argparse.ArgumentParser(description=__doc__)
|
||
parser.add_argument("--dry-run", action="store_true", help="report the next leaf without changing the ledger")
|
||
args = parser.parse_args()
|
||
state = json.loads(STATE.read_text(encoding="utf-8"))
|
||
statuses = state.get("tasks", {})
|
||
active = state.get("active_task")
|
||
# Only parse cards that can participate in this transition. The full card
|
||
# inventory is reserved for the explicit post-implementation validator.
|
||
relevant_ids = [active] if active else []
|
||
relevant_ids.extend(task_id for task_id, status in statuses.items() if status == "planned")
|
||
cards = load_cards(relevant_ids)
|
||
if active and statuses.get(active) == "in_progress":
|
||
if args.dry_run:
|
||
print(f"TASK_CYCLE_ACTIVE: {active} (dry-run; no files changed)")
|
||
return 0
|
||
refresh_feature_registry_if_sources_changed()
|
||
# The outer scheduler prepares the projection before each Codex turn.
|
||
# Rebuild only when the batch/checkpoint fingerprint has changed.
|
||
if batch_requires_refresh():
|
||
prepare_module_batch()
|
||
checkpoint_repaired = run_continuation_check()
|
||
# A repair can change the checkpoint projection, so check once more
|
||
# after validation while preserving an already-ready batch.
|
||
if checkpoint_repaired and batch_requires_refresh():
|
||
prepare_module_batch()
|
||
batch_path = ROOT / "docs/current/module-batch-manifest.json"
|
||
batch = json.loads(batch_path.read_text(encoding="utf-8")) if batch_path.exists() else {}
|
||
if implementation_first_enabled() and batch.get("status") in {"no_ready_batch", "waiting_for_retry"}:
|
||
# The feature registry and runtime-issue ledger are fingerprinted
|
||
# by the planner. Do not rewrite context/checkpoint while idle;
|
||
# that would make the next cycle stale and create perpetual work.
|
||
print(f"TASK_CYCLE_FEATURE_IDLE: active={active} sourceFingerprint={batch.get('sourceFingerprint')}")
|
||
return 0
|
||
if context_requires_refresh(active):
|
||
run("tools/prepare_web_task_context.py")
|
||
else:
|
||
print(f"CONTEXT_PACKET_REUSED: active={active}")
|
||
# Keep the incremental contract check on every cycle, but let it use
|
||
# the manifest's metadata fast path for unchanged inputs.
|
||
if not implementation_first_enabled():
|
||
run("tools/validate_incremental_context.py")
|
||
active_card = cards.get(active)
|
||
if active_card:
|
||
update_checkpoint(active, active_card[2])
|
||
print(f"TASK_CYCLE_CONTINUE: active={active}")
|
||
return 0
|
||
|
||
candidates: list[tuple[str, Path, str]] = []
|
||
for task_id, (path, card_status, text) in cards.items():
|
||
if statuses.get(task_id, card_status) != "planned":
|
||
continue
|
||
if path.parent == ROOT / "docs/tasks/p6":
|
||
continue
|
||
predecessors = predecessor_ids(text)
|
||
if all(statuses.get(item) == "completed" for item in predecessors):
|
||
priority = 0 if task_id.startswith("FW-P6-FPA-IMPL-") else 1
|
||
candidates.append((f"{priority}:{task_id}", path, text))
|
||
if not candidates:
|
||
print("TASK_CYCLE_NO_READY_LEAF: no dependency-ready planned leaf task")
|
||
return 2
|
||
_, path, text = sorted(candidates, key=lambda item: item[0])[0]
|
||
next_task = next(task_id for task_id, (candidate, _, _) in cards.items() if candidate == path)
|
||
if args.dry_run:
|
||
print(f"TASK_CYCLE_NEXT: {next_task} ({path.relative_to(ROOT)})")
|
||
return 0
|
||
|
||
reason = "completed" if active and statuses.get(active) == "completed" else "interrupted"
|
||
run("tools/rotate_web_task_context.py", "--reason", reason, "--next-task", next_task)
|
||
run("tools/sync_web_task_state.py", "--active", next_task)
|
||
update_checkpoint(next_task, text)
|
||
run("tools/check_continuation_state.py")
|
||
prepare_module_batch()
|
||
run("tools/prepare_web_task_context.py")
|
||
if not implementation_first_enabled():
|
||
run("tools/validate_incremental_context.py")
|
||
print(f"TASK_CYCLE_CLAIMED: {next_task} ({path.relative_to(ROOT)})")
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
raise SystemExit(main())
|