✨ feat(workflow): adopt Matt Pocock ticket workflow
Replace the Superpowers plan pipeline with grill-with-docs, specs, local tickets, and ticket-native execution. BREAKING CHANGE: Remove the legacy Plan CLI, prompt templates, and Superpowers skills.
This commit is contained in:
+69
-354
@@ -1,41 +1,22 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import re
|
||||
import sys
|
||||
import tomllib
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from shutil import copy2, copytree, ignore_patterns, rmtree, which
|
||||
import subprocess
|
||||
import importlib.util
|
||||
from typing import Optional
|
||||
|
||||
try:
|
||||
import tomllib
|
||||
except ModuleNotFoundError: # Python < 3.11
|
||||
tomllib = None
|
||||
|
||||
ORDER = [
|
||||
"sync_rules",
|
||||
"sync_memory_bank",
|
||||
"sync_prompts",
|
||||
"sync_standards",
|
||||
"install_skills",
|
||||
"format_md",
|
||||
]
|
||||
SCRIPT_DIR = Path(__file__).resolve().parent
|
||||
PLAYBOOK_ROOT = SCRIPT_DIR.parent
|
||||
MAIN_LOOP_SCRIPT = SCRIPT_DIR / "main_loop.py"
|
||||
MAIN_LOOP_SPEC = importlib.util.spec_from_file_location("playbook_main_loop", MAIN_LOOP_SCRIPT)
|
||||
assert MAIN_LOOP_SPEC and MAIN_LOOP_SPEC.loader
|
||||
MAIN_LOOP = importlib.util.module_from_spec(MAIN_LOOP_SPEC)
|
||||
MAIN_LOOP_SPEC.loader.exec_module(MAIN_LOOP)
|
||||
PATH_CONFIG_KEYS = {
|
||||
"project_root",
|
||||
"playbook_root",
|
||||
"deploy_root",
|
||||
"agents_home",
|
||||
"codex_home",
|
||||
"skill_link",
|
||||
}
|
||||
DOCS_INDEX_SECTION_HEADINGS = {
|
||||
"common": "## 跨语言(common)",
|
||||
"tsl": "## TSL(tsl/tsf)",
|
||||
@@ -46,199 +27,8 @@ DOCS_INDEX_SECTION_HEADINGS = {
|
||||
}
|
||||
|
||||
|
||||
def usage() -> str:
|
||||
return (
|
||||
"Usage:\n"
|
||||
" python scripts/playbook.py -config <path>\n"
|
||||
" python scripts/playbook.py -record-plan <plan_path> -progress <path>\n"
|
||||
" python scripts/playbook.py -h"
|
||||
)
|
||||
|
||||
|
||||
def parse_cli_value(argv: list[str], flag: str) -> Optional[str]:
|
||||
if flag not in argv:
|
||||
return None
|
||||
idx = argv.index(flag)
|
||||
if idx + 1 >= len(argv):
|
||||
return None
|
||||
value = argv[idx + 1].strip()
|
||||
return value or None
|
||||
|
||||
|
||||
def strip_inline_comment(value: str) -> str:
|
||||
in_single = False
|
||||
in_double = False
|
||||
escape = False
|
||||
for idx, ch in enumerate(value):
|
||||
if escape:
|
||||
escape = False
|
||||
continue
|
||||
if in_double and ch == "\\":
|
||||
escape = True
|
||||
continue
|
||||
if ch == "'" and not in_double:
|
||||
in_single = not in_single
|
||||
continue
|
||||
if ch == '"' and not in_single:
|
||||
in_double = not in_double
|
||||
continue
|
||||
if ch == "#" and not in_single and not in_double:
|
||||
return value[:idx].rstrip()
|
||||
return value
|
||||
|
||||
|
||||
def split_list_items(raw: str) -> list[str]:
|
||||
items: list[str] = []
|
||||
buf: list[str] = []
|
||||
in_single = False
|
||||
in_double = False
|
||||
escape = False
|
||||
for ch in raw:
|
||||
if escape:
|
||||
buf.append(ch)
|
||||
escape = False
|
||||
continue
|
||||
if in_double and ch == "\\":
|
||||
buf.append(ch)
|
||||
escape = True
|
||||
continue
|
||||
if ch == "'" and not in_double:
|
||||
in_single = not in_single
|
||||
buf.append(ch)
|
||||
continue
|
||||
if ch == '"' and not in_single:
|
||||
in_double = not in_double
|
||||
buf.append(ch)
|
||||
continue
|
||||
if ch == "," and not in_single and not in_double:
|
||||
items.append("".join(buf).strip())
|
||||
buf = []
|
||||
continue
|
||||
buf.append(ch)
|
||||
tail = "".join(buf).strip()
|
||||
if tail:
|
||||
items.append(tail)
|
||||
return items
|
||||
|
||||
|
||||
def parse_toml_value(raw: str) -> object:
|
||||
value = raw.strip()
|
||||
if not value:
|
||||
return ""
|
||||
if value.startswith("[") and value.endswith("]"):
|
||||
inner = value[1:-1].strip()
|
||||
if not inner:
|
||||
return []
|
||||
return [parse_toml_value(item) for item in split_list_items(inner)]
|
||||
lowered = value.lower()
|
||||
if lowered == "true":
|
||||
return True
|
||||
if lowered == "false":
|
||||
return False
|
||||
if value[0] in ("'", '"') and value[-1] == value[0]:
|
||||
if value[0] == "'":
|
||||
return value[1:-1]
|
||||
import ast
|
||||
|
||||
try:
|
||||
return ast.literal_eval(value)
|
||||
except (ValueError, SyntaxError):
|
||||
return value[1:-1]
|
||||
try:
|
||||
if "." in value:
|
||||
return float(value)
|
||||
return int(value)
|
||||
except ValueError:
|
||||
return value
|
||||
|
||||
|
||||
def loads_toml_minimal(raw: str) -> dict:
|
||||
data: dict[str, dict] = {}
|
||||
current = None
|
||||
for line in raw.splitlines():
|
||||
stripped = line.strip()
|
||||
if not stripped or stripped.startswith("#"):
|
||||
continue
|
||||
if stripped.startswith("[") and stripped.endswith("]"):
|
||||
section = stripped[1:-1].strip()
|
||||
if not section:
|
||||
raise ValueError("empty section header")
|
||||
current = data.setdefault(section, {})
|
||||
if not isinstance(current, dict):
|
||||
raise ValueError(f"invalid section: {section}")
|
||||
continue
|
||||
if "=" not in stripped:
|
||||
raise ValueError(f"invalid line: {line}")
|
||||
key, value = stripped.split("=", 1)
|
||||
key = key.strip()
|
||||
if not key:
|
||||
raise ValueError("missing key")
|
||||
value = strip_inline_comment(value.strip())
|
||||
target = current if current is not None else data
|
||||
target[key] = parse_toml_value(value)
|
||||
return data
|
||||
|
||||
|
||||
def normalize_path_config_strings(raw: str) -> str:
|
||||
normalized_lines: list[str] = []
|
||||
for line in raw.splitlines():
|
||||
stripped = line.strip()
|
||||
if not stripped or stripped.startswith("#") or "=" not in line:
|
||||
normalized_lines.append(line)
|
||||
continue
|
||||
|
||||
key_part, value_part = line.split("=", 1)
|
||||
key = key_part.strip()
|
||||
if key not in PATH_CONFIG_KEYS:
|
||||
normalized_lines.append(line)
|
||||
continue
|
||||
|
||||
value = strip_inline_comment(value_part.strip())
|
||||
if len(value) < 2 or value[0] != '"' or value[-1] != '"' or "\\" not in value[1:-1]:
|
||||
normalized_lines.append(line)
|
||||
continue
|
||||
|
||||
inner = value[1:-1]
|
||||
has_lone_backslash = False
|
||||
probe_idx = 0
|
||||
while probe_idx < len(inner):
|
||||
if inner[probe_idx] != "\\":
|
||||
probe_idx += 1
|
||||
continue
|
||||
if probe_idx + 1 < len(inner) and inner[probe_idx + 1] == "\\":
|
||||
probe_idx += 2
|
||||
continue
|
||||
has_lone_backslash = True
|
||||
break
|
||||
if not has_lone_backslash:
|
||||
normalized_lines.append(line)
|
||||
continue
|
||||
|
||||
escaped: list[str] = []
|
||||
idx = 0
|
||||
while idx < len(inner):
|
||||
ch = inner[idx]
|
||||
if ch != "\\":
|
||||
escaped.append(ch)
|
||||
idx += 1
|
||||
continue
|
||||
if idx + 1 < len(inner) and inner[idx + 1] == "\\":
|
||||
escaped.extend(["\\", "\\"])
|
||||
idx += 2
|
||||
continue
|
||||
escaped.extend(["\\", "\\"])
|
||||
idx += 1
|
||||
normalized_lines.append(f'{key_part}= "{"".join(escaped)}"')
|
||||
|
||||
suffix = "\n" if raw.endswith("\n") else ""
|
||||
return "\n".join(normalized_lines) + suffix
|
||||
|
||||
|
||||
def load_config(path: Path) -> dict:
|
||||
raw = normalize_path_config_strings(path.read_text(encoding="utf-8"))
|
||||
if tomllib is not None:
|
||||
return tomllib.loads(raw)
|
||||
return loads_toml_minimal(raw)
|
||||
return tomllib.loads(path.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def log(message: str) -> None:
|
||||
@@ -300,7 +90,6 @@ def config_uses_playbook_root(config: dict) -> bool:
|
||||
for key in (
|
||||
"sync_rules",
|
||||
"sync_memory_bank",
|
||||
"sync_prompts",
|
||||
"sync_standards",
|
||||
"install_skills",
|
||||
):
|
||||
@@ -309,34 +98,12 @@ def config_uses_playbook_root(config: dict) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def validate_removed_config(config: dict) -> None:
|
||||
if "vendor" in config:
|
||||
raise ValueError(
|
||||
"[vendor] is no longer supported; use [playbook].install_mode = "
|
||||
'"snapshot" and configure languages in [sync_standards]'
|
||||
)
|
||||
|
||||
def resolve_install_mode(config: dict) -> str:
|
||||
playbook_config = config.get("playbook", {})
|
||||
if not isinstance(playbook_config, dict):
|
||||
raise ValueError("[playbook] must be a table")
|
||||
|
||||
if "deploy_root" in playbook_config:
|
||||
raise ValueError(
|
||||
"playbook.deploy_root is no longer supported; use playbook.playbook_root"
|
||||
)
|
||||
if "intall_mode" in playbook_config:
|
||||
raise ValueError(
|
||||
"playbook.intall_mode is not supported; use playbook.install_mode"
|
||||
)
|
||||
|
||||
|
||||
def resolve_install_mode(config: dict) -> str:
|
||||
playbook_config = config.get("playbook", {})
|
||||
raw = "subtree"
|
||||
if (
|
||||
isinstance(playbook_config, dict)
|
||||
and playbook_config.get("install_mode") is not None
|
||||
):
|
||||
if playbook_config.get("install_mode") is not None:
|
||||
raw = playbook_config.get("install_mode")
|
||||
mode = str(raw).strip().lower()
|
||||
if mode not in ("subtree", "snapshot"):
|
||||
@@ -458,7 +225,7 @@ def write_snapshot_readme(
|
||||
f"python {scripts_path} -config playbook.toml",
|
||||
"```",
|
||||
"",
|
||||
f"配置示例:`{join_playbook_subpath(playbook_root, 'playbook.toml.example')}`",
|
||||
f"配置示例:`{join_playbook_subpath(playbook_root, 'playbook.example.toml')}`",
|
||||
"",
|
||||
"文档入口:",
|
||||
"",
|
||||
@@ -600,9 +367,9 @@ def install_snapshot(config: dict, context: dict) -> int:
|
||||
if templates_src.is_dir():
|
||||
copytree(templates_src, dest_prefix / "templates" / lang)
|
||||
|
||||
example_config = PLAYBOOK_ROOT / "playbook.toml.example"
|
||||
example_config = PLAYBOOK_ROOT / "playbook.example.toml"
|
||||
if example_config.is_file():
|
||||
copy2(example_config, dest_prefix / "playbook.toml.example")
|
||||
copy2(example_config, dest_prefix / "playbook.example.toml")
|
||||
|
||||
write_docs_index(dest_prefix, langs)
|
||||
write_snapshot_readme(dest_prefix, playbook_root, langs)
|
||||
@@ -680,6 +447,13 @@ def resolve_template_target(
|
||||
return target_root / rel
|
||||
|
||||
|
||||
def format_sync_counts(label: str, written: int, skipped: int, force: bool) -> str:
|
||||
message = f"Synced: {label} (written={written}, skipped={skipped})"
|
||||
if skipped and not force:
|
||||
message += " Existing files kept; use force to overwrite."
|
||||
return message
|
||||
|
||||
|
||||
def sync_directory(
|
||||
template_dir: Path,
|
||||
target_dir: Path,
|
||||
@@ -689,7 +463,9 @@ def sync_directory(
|
||||
playbook_root: str | None,
|
||||
force: bool,
|
||||
no_backup: bool,
|
||||
) -> None:
|
||||
) -> tuple[int, int]:
|
||||
written = 0
|
||||
skipped = 0
|
||||
for template_file in template_dir.rglob("*"):
|
||||
if not template_file.is_file():
|
||||
continue
|
||||
@@ -699,6 +475,7 @@ def sync_directory(
|
||||
ensure_dir(target_file.parent)
|
||||
if target_file.exists():
|
||||
if not force:
|
||||
skipped += 1
|
||||
continue
|
||||
backup_path(target_file, no_backup)
|
||||
copy2(template_file, target_file)
|
||||
@@ -709,6 +486,8 @@ def sync_directory(
|
||||
playbook_scripts,
|
||||
playbook_root,
|
||||
)
|
||||
written += 1
|
||||
return written, skipped
|
||||
|
||||
|
||||
def extract_block_lines(text: str, start: str, end: str) -> list[str]:
|
||||
@@ -727,6 +506,30 @@ def extract_block_lines(text: str, start: str, end: str) -> list[str]:
|
||||
return block
|
||||
|
||||
|
||||
_AGENTS_BLOCK_START = "<!-- playbook:agents:start -->"
|
||||
_AGENTS_BLOCK_END = "<!-- playbook:agents:end -->"
|
||||
|
||||
|
||||
def preserve_agents_subblock(block: list[str], agents_text: str) -> list[str]:
|
||||
existing = extract_block_lines(agents_text, _AGENTS_BLOCK_START, _AGENTS_BLOCK_END)
|
||||
if not existing:
|
||||
return block
|
||||
|
||||
start_index: int | None = None
|
||||
end_index: int | None = None
|
||||
for index, line in enumerate(block):
|
||||
stripped = line.strip()
|
||||
if stripped == _AGENTS_BLOCK_START:
|
||||
start_index = index
|
||||
elif stripped == _AGENTS_BLOCK_END and start_index is not None:
|
||||
end_index = index
|
||||
break
|
||||
if start_index is None or end_index is None:
|
||||
return block
|
||||
|
||||
return block[:start_index] + existing + block[end_index + 1 :]
|
||||
|
||||
|
||||
def update_agents_section(
|
||||
agents_path: Path,
|
||||
template_path: Path,
|
||||
@@ -747,12 +550,15 @@ def update_agents_section(
|
||||
return
|
||||
|
||||
if not agents_path.exists():
|
||||
agents_path.write_text(template_text + "\n", encoding="utf-8", newline="\n")
|
||||
agents_path.write_text(
|
||||
template_text.rstrip("\n") + "\n", encoding="utf-8", newline="\n"
|
||||
)
|
||||
log("Created: AGENTS.md")
|
||||
return
|
||||
|
||||
agents_text = agents_path.read_text(encoding="utf-8")
|
||||
if start_marker in agents_text:
|
||||
block = preserve_agents_subblock(block, agents_text)
|
||||
lines = agents_text.splitlines()
|
||||
updated: list[str] = []
|
||||
in_block = False
|
||||
@@ -773,9 +579,6 @@ def update_agents_section(
|
||||
)
|
||||
log("Updated: AGENTS.md (section)")
|
||||
else:
|
||||
if ".agents/index.md" in agents_text:
|
||||
log("Skip: AGENTS.md already references .agents/index.md")
|
||||
return
|
||||
updated = agents_text.rstrip("\n") + "\n\n" + "\n".join(block) + "\n"
|
||||
agents_path.write_text(updated, encoding="utf-8", newline="\n")
|
||||
log("Appended: AGENTS.md (section)")
|
||||
@@ -796,7 +599,7 @@ def resolve_project_name(context: dict) -> str | None:
|
||||
def resolve_template_date(context: dict) -> str:
|
||||
config = context.get("config", {})
|
||||
if isinstance(config, dict):
|
||||
for key in ("sync_rules", "sync_memory_bank", "sync_prompts"):
|
||||
for key in ("sync_rules", "sync_memory_bank"):
|
||||
section = config.get(key)
|
||||
if isinstance(section, dict):
|
||||
value = section.get("date")
|
||||
@@ -821,27 +624,11 @@ def sync_agents_template(context: dict) -> int:
|
||||
playbook_root = resolve_playbook_root(context)
|
||||
date_value = resolve_template_date(context)
|
||||
|
||||
agents_dst = project_root / "AGENTS.md"
|
||||
if agents_dst.exists():
|
||||
agents_text = agents_dst.read_text(encoding="utf-8")
|
||||
if "<!-- playbook:framework:start -->" in agents_text:
|
||||
start_marker = "<!-- playbook:framework:start -->"
|
||||
end_marker = "<!-- playbook:framework:end -->"
|
||||
elif "<!-- playbook:templates:start -->" in agents_text:
|
||||
start_marker = "<!-- playbook:templates:start -->"
|
||||
end_marker = "<!-- playbook:templates:end -->"
|
||||
else:
|
||||
start_marker = "<!-- playbook:templates:start -->"
|
||||
end_marker = "<!-- playbook:templates:end -->"
|
||||
else:
|
||||
start_marker = "<!-- playbook:framework:start -->"
|
||||
end_marker = "<!-- playbook:framework:end -->"
|
||||
|
||||
update_agents_section(
|
||||
agents_dst,
|
||||
project_root / "AGENTS.md",
|
||||
agents_src,
|
||||
start_marker,
|
||||
end_marker,
|
||||
"<!-- playbook:framework:start -->",
|
||||
"<!-- playbook:framework:end -->",
|
||||
project_name,
|
||||
date_value,
|
||||
playbook_scripts,
|
||||
@@ -927,8 +714,6 @@ def sync_claude_md(project_root: Path, config: dict) -> None:
|
||||
updated_text = "# CLAUDE.md\n\n" + updated_text.lstrip()
|
||||
claude_md.write_text(updated_text, encoding="utf-8", newline="\n")
|
||||
log("Updated CLAUDE.md (playbook block).")
|
||||
elif "@AGENTS.md" in text:
|
||||
log("Skip: CLAUDE.md already references AGENTS.md")
|
||||
else:
|
||||
appended = text.rstrip("\n") + "\n\n" + "\n".join(block_lines) + "\n"
|
||||
claude_md.write_text(appended, encoding="utf-8", newline="\n")
|
||||
@@ -936,7 +721,7 @@ def sync_claude_md(project_root: Path, config: dict) -> None:
|
||||
|
||||
|
||||
def should_sync_agents(config: dict) -> bool:
|
||||
for key in ("sync_rules", "sync_memory_bank", "sync_prompts", "sync_standards"):
|
||||
for key in ("sync_rules", "sync_memory_bank", "sync_standards"):
|
||||
if key in config:
|
||||
return True
|
||||
return False
|
||||
@@ -1015,7 +800,7 @@ def sync_memory_bank_action(config: dict, context: dict) -> int:
|
||||
|
||||
memory_dst = project_root / "memory-bank"
|
||||
ensure_dir(memory_dst)
|
||||
sync_directory(
|
||||
written, skipped = sync_directory(
|
||||
memory_src,
|
||||
memory_dst,
|
||||
project_name,
|
||||
@@ -1025,43 +810,7 @@ def sync_memory_bank_action(config: dict, context: dict) -> int:
|
||||
force,
|
||||
no_backup,
|
||||
)
|
||||
log("Synced: memory-bank/")
|
||||
return 0
|
||||
|
||||
|
||||
def sync_prompts_action(config: dict, context: dict) -> int:
|
||||
project_root: Path = context["project_root"]
|
||||
if project_root.resolve() == PLAYBOOK_ROOT.resolve():
|
||||
log("Skip: playbook root equals project root.")
|
||||
return 0
|
||||
|
||||
templates_dir = PLAYBOOK_ROOT / "templates"
|
||||
prompts_src = templates_dir / "prompts"
|
||||
if not prompts_src.is_dir():
|
||||
print(f"ERROR: templates not found: {prompts_src}", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
project_name = resolve_project_name(context)
|
||||
playbook_scripts = resolve_playbook_scripts(context)
|
||||
playbook_root = resolve_playbook_root(context)
|
||||
date_value = config.get("date") or datetime.now().strftime("%Y-%m-%d")
|
||||
force = bool(config.get("force", False))
|
||||
no_backup = bool(config.get("no_backup", False))
|
||||
|
||||
prompts_dst = project_root / "docs/prompts"
|
||||
ensure_dir(prompts_dst.parent)
|
||||
ensure_dir(prompts_dst)
|
||||
sync_directory(
|
||||
prompts_src,
|
||||
prompts_dst,
|
||||
project_name,
|
||||
date_value,
|
||||
playbook_scripts,
|
||||
playbook_root,
|
||||
force,
|
||||
no_backup,
|
||||
)
|
||||
log("Synced: docs/prompts/")
|
||||
log(format_sync_counts("memory-bank/", written, skipped, force))
|
||||
return 0
|
||||
|
||||
|
||||
@@ -1075,6 +824,7 @@ def render_agents_block(langs: list[str]) -> list[str]:
|
||||
"",
|
||||
"- 入口:`.agents/index.md`",
|
||||
f"- 语言规则:{langs_line}" if langs_line else "- 语言规则:",
|
||||
"",
|
||||
"<!-- playbook:agents:end -->",
|
||||
]
|
||||
return lines
|
||||
@@ -1109,9 +859,6 @@ def update_agents_block(agents_md: Path, block_lines: list[str]) -> None:
|
||||
agents_md.write_text("\n".join(updated) + "\n", encoding="utf-8", newline="\n")
|
||||
log("Updated AGENTS.md (playbook block).")
|
||||
else:
|
||||
if ".agents/index.md" in text:
|
||||
log("Skip: AGENTS.md already references .agents/index.md")
|
||||
return
|
||||
updated = text.rstrip("\n") + "\n\n" + "\n".join(block_lines) + "\n"
|
||||
agents_md.write_text(updated, encoding="utf-8", newline="\n")
|
||||
log("Appended playbook block to AGENTS.md")
|
||||
@@ -1234,8 +981,6 @@ def sync_gitattributes_append(
|
||||
def sync_gitattributes_block(src: Path, dst: Path, no_backup: bool) -> None:
|
||||
begin = "# BEGIN playbook .gitattributes"
|
||||
end = "# END playbook .gitattributes"
|
||||
begin_old = "# BEGIN tsl-playbook .gitattributes"
|
||||
end_old = "# END tsl-playbook .gitattributes"
|
||||
|
||||
src_lines = src.read_text(encoding="utf-8").splitlines()
|
||||
block_lines = [begin] + src_lines + [end]
|
||||
@@ -1246,14 +991,14 @@ def sync_gitattributes_block(src: Path, dst: Path, no_backup: bool) -> None:
|
||||
in_block = False
|
||||
replaced = False
|
||||
for line in original:
|
||||
if line == begin or line == begin_old:
|
||||
if line == begin:
|
||||
if not replaced:
|
||||
updated.extend(block_lines)
|
||||
replaced = True
|
||||
in_block = True
|
||||
continue
|
||||
if in_block:
|
||||
if line == end or line == end_old:
|
||||
if line == end:
|
||||
in_block = False
|
||||
continue
|
||||
updated.append(line)
|
||||
@@ -1369,9 +1114,6 @@ def normalize_globs(raw: object) -> list[str]:
|
||||
|
||||
def install_skills_action(config: dict, context: dict) -> int:
|
||||
mode = str(config.get("mode", "list")).lower()
|
||||
if "codex_home" in config:
|
||||
print("ERROR: codex_home is no longer supported; use agents_home", file=sys.stderr)
|
||||
return 2
|
||||
agents_home = Path(config.get("agents_home", "~/.agents")).expanduser()
|
||||
if not agents_home.is_absolute():
|
||||
agents_home = (context["project_root"] / agents_home).resolve()
|
||||
@@ -1521,8 +1263,6 @@ def run_action(name: str, config: dict, context: dict) -> int:
|
||||
return sync_rules_action(config, context)
|
||||
if name == "sync_memory_bank":
|
||||
return sync_memory_bank_action(config, context)
|
||||
if name == "sync_prompts":
|
||||
return sync_prompts_action(config, context)
|
||||
if name == "sync_standards":
|
||||
return sync_standards_action(config, context)
|
||||
if name == "install_skills":
|
||||
@@ -1533,47 +1273,22 @@ def run_action(name: str, config: dict, context: dict) -> int:
|
||||
|
||||
|
||||
def main(argv: list[str]) -> int:
|
||||
if "-h" in argv or "-help" in argv:
|
||||
print(usage())
|
||||
return 0
|
||||
parser = argparse.ArgumentParser(prog="playbook.py")
|
||||
parser.add_argument("-config", dest="config_path", required=True, metavar="PATH")
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
spec_path = parse_cli_value(argv, "-record-spec")
|
||||
if spec_path is not None:
|
||||
print(
|
||||
"ERROR: -record-spec has been removed; spec files are the record.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 2
|
||||
|
||||
plan_path = parse_cli_value(argv, "-record-plan")
|
||||
if plan_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_plan(Path(progress_path), plan_path)
|
||||
if code != 0:
|
||||
print(message, file=sys.stderr)
|
||||
return code
|
||||
print(message)
|
||||
return 0
|
||||
|
||||
if "-config" not in argv:
|
||||
print("ERROR: -config is required.\n" + usage(), file=sys.stderr)
|
||||
return 2
|
||||
idx = argv.index("-config")
|
||||
if idx + 1 >= len(argv) or not argv[idx + 1]:
|
||||
print("ERROR: -config requires a path.\n" + usage(), file=sys.stderr)
|
||||
return 2
|
||||
|
||||
config_path = Path(argv[idx + 1]).expanduser()
|
||||
config_path = Path(args.config_path).expanduser()
|
||||
if not config_path.is_file():
|
||||
print(f"ERROR: config not found: {config_path}", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
config = load_config(config_path)
|
||||
try:
|
||||
validate_removed_config(config)
|
||||
config = load_config(config_path)
|
||||
except tomllib.TOMLDecodeError as exc:
|
||||
print(f"ERROR: invalid TOML in {config_path}: {exc}", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
try:
|
||||
install_mode = resolve_install_mode(config)
|
||||
except ValueError as exc:
|
||||
print(f"ERROR: {exc}", file=sys.stderr)
|
||||
|
||||
Reference in New Issue
Block a user