Merge commit '3d83740f88b6aaba6962256be517656496b83f25' into lsp-server

This commit is contained in:
csh
2026-05-24 13:04:22 +08:00
292 changed files with 30774 additions and 218828 deletions
+5 -3
View File
@@ -10,13 +10,15 @@ tests/
├── cli/ # Python CLI 测试(unittest
│ └── test_playbook_cli.py # playbook.py 基础功能测试
├── test_format_md_action.py # format_md 动作测试
├── test_gitea_workflow_bootstrap.py # Gitea workflow 自举顺序回归测试
├── test_firstparty_skills_quality.py # first-party skills 元数据与结构质量测试
├── test_gitattributes_modes.py # gitattr_mode 行为测试
├── test_no_backup_flags.py # no_backup 行为测试
├── test_playbook_typing_imports.py # playbook.py typing 导入兼容性测试
├── test_sync_directory_actions.py # sync_memory_bank/sync_prompts 行为测试
├── test_vendor_snapshot_templates.py # vendor 快照模板完整性测试
├── test_plan_progress_cli.py # plan_progress CLI 测试
├── test_superpowers_list_sync.py # superpowers 列表一致性测试
├── test_superpowers_workflows.py # superpowers 工作流配置校验
├── test_main_loop_cli.py # main_loop CLI 测试
├── test_thirdparty_skills_pipeline.py # thirdparty skills 流水线配置与同步产物测试
├── test_sync_templates_placeholders.py # 占位符替换测试(sync_rules/sync_standards
├── test_toml_edge_cases.py # TOML 解析边界测试
├── templates/ # 模板验证测试
@@ -6,31 +6,124 @@ from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
SCRIPT = ROOT / "scripts" / "playbook.py"
CUSTOM_DEPLOY_ROOT = "custom/playbook"
def run_cli(*args):
def run_script(script, *args):
return subprocess.run(
[sys.executable, str(SCRIPT), *args],
[sys.executable, str(script), *args],
capture_output=True,
text=True,
)
def run_cli(*args):
return run_script(SCRIPT, *args)
def write_config(root: Path, name: str, body: str) -> Path:
config_path = root / name
config_path.write_text(body, encoding="utf-8")
return config_path
def bash_path(path: Path) -> str:
resolved = path.resolve()
if sys.platform != "win32":
return resolved.as_posix()
drive = resolved.drive.rstrip(":").lower()
rest = resolved.as_posix()[2:]
return f"/mnt/{drive}{rest}"
class PlaybookCliTests(unittest.TestCase):
def assert_style_cleanup_tsl_docs_prefix(
self, root: Path, agents_home: Path, docs_prefix: str
) -> None:
agents_index = (root / ".agents" / "tsl" / "index.md").read_text(encoding="utf-8")
self.assertIn(f"`{docs_prefix}/tsl/index.md`", agents_index)
self.assertNotIn("`docs/tsl/index.md`", agents_index)
skill_file = (agents_home / "skills" / "style-cleanup" / "SKILL.md").read_text(
encoding="utf-8"
)
self.assertIn(f"`{docs_prefix}/tsl/code_style.md`", skill_file)
self.assertNotIn("`docs/tsl/code_style.md`", skill_file)
def test_help_shows_usage(self):
result = run_cli("-h")
self.assertEqual(result.returncode, 0)
self.assertIn("Usage:", result.stdout + result.stderr)
def test_record_spec_updates_progress_workflow_state(self):
with tempfile.TemporaryDirectory() as tmp_dir:
root = Path(tmp_dir)
progress = root / "memory-bank" / "progress.md"
progress.parent.mkdir(parents=True)
progress.write_text("# 当前进展\n", encoding="utf-8")
result = run_cli(
"-record-spec",
"docs/superpowers/specs/2026-05-18-demo-design.md",
"-progress",
str(progress),
)
self.assertEqual(result.returncode, 0, msg=result.stdout + result.stderr)
text = progress.read_text(encoding="utf-8")
self.assertIn("phase: planning", text)
self.assertIn("spec: docs/superpowers/specs/2026-05-18-demo-design.md", text)
def test_record_plan_updates_progress_workflow_state(self):
with tempfile.TemporaryDirectory() as tmp_dir:
root = Path(tmp_dir)
progress = root / "memory-bank" / "progress.md"
progress.parent.mkdir(parents=True)
progress.write_text(
"\n".join(
[
"# 当前进展",
"",
"## Workflow State",
"",
"<!-- workflow-state:start -->",
"phase: planning",
"spec: docs/superpowers/specs/2026-05-18-demo-design.md",
"<!-- workflow-state:end -->",
]
)
+ "\n",
encoding="utf-8",
)
result = run_cli(
"-record-plan",
"docs/superpowers/plans/2026-05-18-demo.md",
"-progress",
str(progress),
)
self.assertEqual(result.returncode, 0, msg=result.stdout + result.stderr)
text = progress.read_text(encoding="utf-8")
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_missing_config_is_error(self):
result = run_cli()
self.assertNotEqual(result.returncode, 0)
self.assertIn("-config", result.stdout + result.stderr)
def test_action_order(self):
config_body = """
config_body = f"""
[playbook]
project_root = "."
deploy_root = "{CUSTOM_DEPLOY_ROOT}"
[format_md]
@@ -47,8 +140,68 @@ langs = ["tsl"]
self.assertIn("sync_standards", output)
self.assertIn("format_md", output)
def test_format_md_only_does_not_require_deploy_root(self):
with tempfile.TemporaryDirectory() as tmp_dir:
config_body = f"""
[playbook]
project_root = "{tmp_dir}"
[format_md]
"""
config_path = Path(tmp_dir) / "playbook.toml"
config_path.write_text(config_body, encoding="utf-8")
result = run_cli("-config", str(config_path))
self.assertEqual(result.returncode, 0)
def test_vendor_creates_snapshot(self):
with tempfile.TemporaryDirectory() as tmp_dir:
root = Path(tmp_dir)
config_body = f"""
[playbook]
project_root = "{tmp_dir}"
deploy_root = "{CUSTOM_DEPLOY_ROOT}"
[vendor]
langs = ["tsl"]
"""
config_path = write_config(root, "playbook.toml", config_body)
result = run_cli("-config", str(config_path))
snapshot = root / CUSTOM_DEPLOY_ROOT / "SOURCE.md"
self.assertEqual(result.returncode, 0)
self.assertTrue(snapshot.is_file())
def test_vendor_docs_index_uses_new_tsl_entrypoints(self):
with tempfile.TemporaryDirectory() as tmp_dir:
root = Path(tmp_dir)
config_body = f"""
[playbook]
project_root = "{tmp_dir}"
deploy_root = "{CUSTOM_DEPLOY_ROOT}"
[vendor]
langs = ["tsl"]
"""
config_path = write_config(root, "playbook.toml", config_body)
result = run_cli("-config", str(config_path))
docs_index = root / CUSTOM_DEPLOY_ROOT / "docs/index.md"
self.assertEqual(result.returncode, 0)
text = docs_index.read_text(encoding="utf-8")
self.assertIn("`tsl/index.md`", text)
self.assertIn("`tsl/syntax/index.md`", text)
self.assertIn("`tsl/finance/index.md`", text)
self.assertIn("`tsl/modules/index.md`", text)
self.assertIn("`tsl/reference/index.md`", text)
self.assertNotIn("`tsl/syntax_book/index.md`", text)
def test_external_clone_requires_explicit_deploy_root(self):
with tempfile.TemporaryDirectory() as tmp_dir:
root = Path(tmp_dir)
config_body = f"""
[playbook]
project_root = "{tmp_dir}"
@@ -56,20 +209,19 @@ project_root = "{tmp_dir}"
[vendor]
langs = ["tsl"]
"""
config_path = Path(tmp_dir) / "playbook.toml"
config_path.write_text(config_body, encoding="utf-8")
config_path = write_config(root, "playbook.toml", config_body)
result = run_cli("-config", str(config_path))
snapshot = Path(tmp_dir) / "docs/standards/playbook/SOURCE.md"
self.assertEqual(result.returncode, 0)
self.assertTrue(snapshot.is_file())
self.assertNotEqual(result.returncode, 0)
self.assertIn("deploy_root", result.stdout + result.stderr)
def test_sync_memory_bank_creates_memory_bank(self):
with tempfile.TemporaryDirectory() as tmp_dir:
config_body = f"""
[playbook]
project_root = "{tmp_dir}"
deploy_root = "{CUSTOM_DEPLOY_ROOT}"
[sync_memory_bank]
project_name = "Demo"
@@ -88,6 +240,7 @@ project_name = "Demo"
config_body = f"""
[playbook]
project_root = "{tmp_dir}"
deploy_root = "{CUSTOM_DEPLOY_ROOT}"
[sync_standards]
langs = ["tsl"]
@@ -101,11 +254,77 @@ langs = ["tsl"]
self.assertEqual(result.returncode, 0)
self.assertTrue(agents_index.is_file())
def test_sync_standards_updates_agents_index_when_langs_expand(self):
with tempfile.TemporaryDirectory() as tmp_dir:
root = Path(tmp_dir)
first_config = root / "playbook-first.toml"
first_config.write_text(
f"""
[playbook]
project_root = "{tmp_dir}"
deploy_root = "{CUSTOM_DEPLOY_ROOT}"
[sync_standards]
langs = ["tsl"]
no_backup = true
""",
encoding="utf-8",
)
first_result = run_cli("-config", str(first_config))
self.assertEqual(first_result.returncode, 0)
second_config = root / "playbook-second.toml"
second_config.write_text(
f"""
[playbook]
project_root = "{tmp_dir}"
deploy_root = "{CUSTOM_DEPLOY_ROOT}"
[sync_standards]
langs = ["tsl", "cpp"]
no_backup = true
""",
encoding="utf-8",
)
second_result = run_cli("-config", str(second_config))
self.assertEqual(second_result.returncode, 0)
agents_index = (root / ".agents" / "index.md").read_text(encoding="utf-8")
self.assertIn("`.agents/tsl/index.md`", agents_index)
self.assertIn("`.agents/cpp/index.md`", agents_index)
def test_sync_standards_agents_index_only_lists_configured_langs(self):
with tempfile.TemporaryDirectory() as tmp_dir:
root = Path(tmp_dir)
config_body = f"""
[playbook]
project_root = "{tmp_dir}"
deploy_root = "{CUSTOM_DEPLOY_ROOT}"
[sync_standards]
langs = ["tsl", "markdown"]
"""
config_path = write_config(root, "playbook.toml", config_body)
result = run_cli("-config", str(config_path))
self.assertEqual(result.returncode, 0, msg=result.stdout + result.stderr)
agents_index = (root / ".agents" / "index.md").read_text(encoding="utf-8")
self.assertIn("`.agents/tsl/`TSL 相关规则集", agents_index)
self.assertIn("`.agents/markdown/`Markdown 相关规则集", agents_index)
self.assertNotIn("`.agents/cpp/`", agents_index)
self.assertNotIn("`.agents/python/`", agents_index)
self.assertNotIn("`.agents/typescript/`", agents_index)
def test_sync_standards_agents_block_has_blank_lines(self):
with tempfile.TemporaryDirectory() as tmp_dir:
config_body = f"""
[playbook]
project_root = "{tmp_dir}"
deploy_root = "{CUSTOM_DEPLOY_ROOT}"
[sync_standards]
langs = ["tsl"]
@@ -131,6 +350,7 @@ langs = ["tsl"]
config_body = f"""
[playbook]
project_root = "{tmp_dir}"
deploy_root = "{CUSTOM_DEPLOY_ROOT}"
[install_skills]
agents_home = "{target}"
@@ -146,12 +366,98 @@ skills = ["brainstorming"]
self.assertEqual(result.returncode, 0)
self.assertTrue(skill_file.is_file())
def test_install_generated_thirdparty_karpathy_skill_after_sync(self):
with tempfile.TemporaryDirectory() as tmp_dir:
tmp_root = Path(tmp_dir)
mirror = tmp_root / "origin.git"
repo = tmp_root / "repo"
target = tmp_root / "agents"
clone_mirror = subprocess.run(
["git", "clone", "--mirror", str(ROOT), str(mirror)],
capture_output=True,
text=True,
)
self.assertEqual(clone_mirror.returncode, 0, msg=clone_mirror.stderr)
clone_repo = subprocess.run(
["git", "clone", str(mirror), str(repo)],
capture_output=True,
text=True,
)
self.assertEqual(clone_repo.returncode, 0, msg=clone_repo.stderr)
set_remote = subprocess.run(
["git", "-C", str(repo), "remote", "set-url", "origin", bash_path(mirror)],
capture_output=True,
text=True,
)
self.assertEqual(set_remote.returncode, 0, msg=set_remote.stderr)
manifest_src = ROOT / ".gitea" / "ci" / "thirdparty_skills.json"
manifest_dst = repo / ".gitea" / "ci" / "thirdparty_skills.json"
manifest_dst.write_text(manifest_src.read_text(encoding="utf-8"), encoding="utf-8")
sync_src = ROOT / ".gitea" / "ci" / "sync_thirdparty_skills.sh"
sync_dst = repo / ".gitea" / "ci" / "sync_thirdparty_skills.sh"
sync_dst.write_text(sync_src.read_text(encoding="utf-8"), encoding="utf-8")
sync_result = subprocess.run(
["bash", ".gitea/ci/sync_thirdparty_skills.sh"],
cwd=repo,
capture_output=True,
text=True,
)
self.assertEqual(
sync_result.returncode, 0, msg=sync_result.stdout + sync_result.stderr
)
config_body = f"""
[playbook]
project_root = "{tmp_root}"
deploy_root = "{CUSTOM_DEPLOY_ROOT}"
[install_skills]
agents_home = "{target}"
mode = "list"
skills = ["karpathy-guidelines"]
"""
config_path = tmp_root / "playbook.toml"
config_path.write_text(config_body, encoding="utf-8")
result = run_script(repo / "scripts" / "playbook.py", "-config", str(config_path))
skill_file = target / "skills" / "karpathy-guidelines" / "SKILL.md"
self.assertEqual(result.returncode, 0, msg=result.stdout + result.stderr)
self.assertTrue(skill_file.is_file())
def test_install_skills_rejects_removed_tsl_guide(self):
with tempfile.TemporaryDirectory() as tmp_dir:
target = Path(tmp_dir) / "agents"
config_body = f"""
[playbook]
project_root = "{tmp_dir}"
deploy_root = "{CUSTOM_DEPLOY_ROOT}"
[install_skills]
agents_home = "{target}"
mode = "list"
skills = ["tsl-guide"]
"""
config_path = Path(tmp_dir) / "playbook.toml"
config_path.write_text(config_body, encoding="utf-8")
result = run_cli("-config", str(config_path))
self.assertNotEqual(result.returncode, 0)
self.assertIn("skill not found: tsl-guide", result.stdout + result.stderr)
def test_install_skills_rejects_codex_home(self):
with tempfile.TemporaryDirectory() as tmp_dir:
target = Path(tmp_dir) / "codex"
config_body = f"""
[playbook]
project_root = "{tmp_dir}"
deploy_root = "{CUSTOM_DEPLOY_ROOT}"
[install_skills]
codex_home = "{target}"
@@ -166,5 +472,263 @@ skills = ["brainstorming"]
self.assertNotEqual(result.returncode, 0)
self.assertIn("codex_home", result.stdout + result.stderr)
def test_external_clone_flow_rewrites_links_with_configured_deploy_root(self):
with tempfile.TemporaryDirectory() as tmp_dir:
root = Path(tmp_dir)
agents_home = root / "agents-home"
config_body = f"""
[playbook]
project_root = "{tmp_dir}"
deploy_root = "{CUSTOM_DEPLOY_ROOT}"
[vendor]
langs = ["tsl"]
[sync_standards]
langs = ["tsl"]
no_backup = true
[install_skills]
agents_home = "{agents_home}"
mode = "list"
skills = ["style-cleanup"]
"""
config_path = write_config(root, "playbook.toml", config_body)
result = run_cli("-config", str(config_path))
self.assertEqual(result.returncode, 0, msg=result.stdout + result.stderr)
self.assertTrue((root / CUSTOM_DEPLOY_ROOT / "SOURCE.md").is_file())
self.assert_style_cleanup_tsl_docs_prefix(
root, agents_home, f"{CUSTOM_DEPLOY_ROOT}/docs"
)
def test_deployed_snapshot_rewrites_links_from_snapshot_location(self):
with tempfile.TemporaryDirectory() as tmp_dir:
root = Path(tmp_dir)
vendor_config = write_config(
root,
"vendor.toml",
f"""
[playbook]
project_root = "{tmp_dir}"
deploy_root = "{CUSTOM_DEPLOY_ROOT}"
[vendor]
langs = ["tsl"]
""",
)
vendor_result = run_cli("-config", str(vendor_config))
self.assertEqual(vendor_result.returncode, 0, msg=vendor_result.stdout + vendor_result.stderr)
vendored_script = root / CUSTOM_DEPLOY_ROOT / "scripts" / "playbook.py"
agents_home = root / "local-agents"
sync_config = write_config(
root,
"sync.toml",
f"""
[playbook]
project_root = "{tmp_dir}"
deploy_root = "{CUSTOM_DEPLOY_ROOT}"
[sync_standards]
langs = ["tsl"]
no_backup = true
[install_skills]
agents_home = "{agents_home}"
mode = "list"
skills = ["style-cleanup"]
""",
)
sync_result = run_script(vendored_script, "-config", str(sync_config))
self.assertEqual(sync_result.returncode, 0, msg=sync_result.stdout + sync_result.stderr)
self.assert_style_cleanup_tsl_docs_prefix(
root, agents_home, f"{CUSTOM_DEPLOY_ROOT}/docs"
)
def test_sync_claude_md_creates_when_no_claude_md(self):
with tempfile.TemporaryDirectory() as tmp_dir:
config_body = f"""
[playbook]
project_root = "{tmp_dir}"
deploy_root = "{CUSTOM_DEPLOY_ROOT}"
[sync_memory_bank]
project_name = "Demo"
"""
config_path = Path(tmp_dir) / "playbook.toml"
config_path.write_text(config_body, encoding="utf-8")
result = run_cli("-config", str(config_path))
self.assertEqual(result.returncode, 0)
claude_md = Path(tmp_dir) / "CLAUDE.md"
self.assertTrue(claude_md.exists())
text = claude_md.read_text(encoding="utf-8")
self.assertIn("@AGENTS.md", text)
self.assertIn("<!-- playbook:claude:start -->", text)
def test_sync_claude_md_appends_block_to_existing_claude_md(self):
with tempfile.TemporaryDirectory() as tmp_dir:
claude_md = Path(tmp_dir) / "CLAUDE.md"
claude_md.write_text("# My project\n\nSome existing content.\n", encoding="utf-8")
config_body = f"""
[playbook]
project_root = "{tmp_dir}"
deploy_root = "{CUSTOM_DEPLOY_ROOT}"
[sync_memory_bank]
project_name = "Demo"
"""
config_path = Path(tmp_dir) / "playbook.toml"
config_path.write_text(config_body, encoding="utf-8")
result = run_cli("-config", str(config_path))
self.assertEqual(result.returncode, 0)
text = claude_md.read_text(encoding="utf-8")
self.assertIn("@AGENTS.md", text)
self.assertIn("@AGENT_RULES.md", text)
self.assertIn("<!-- playbook:claude:start -->", text)
self.assertIn("Some existing content.", text)
def test_sync_claude_md_updates_existing_block(self):
with tempfile.TemporaryDirectory() as tmp_dir:
claude_md = Path(tmp_dir) / "CLAUDE.md"
claude_md.write_text(
"# My project\n\n"
"<!-- playbook:claude:start -->\n"
"@AGENTS.md\n"
"<!-- playbook:claude:end -->\n",
encoding="utf-8",
)
config_body = f"""
[playbook]
project_root = "{tmp_dir}"
deploy_root = "{CUSTOM_DEPLOY_ROOT}"
[sync_memory_bank]
project_name = "Demo"
"""
config_path = Path(tmp_dir) / "playbook.toml"
config_path.write_text(config_body, encoding="utf-8")
result = run_cli("-config", str(config_path))
self.assertEqual(result.returncode, 0)
text = claude_md.read_text(encoding="utf-8")
self.assertIn("@AGENTS.md", text)
self.assertIn("@AGENT_RULES.md", text)
self.assertEqual(text.count("<!-- playbook:claude:start -->"), 1)
def test_sync_claude_md_skips_when_already_references_agents(self):
with tempfile.TemporaryDirectory() as tmp_dir:
claude_md = Path(tmp_dir) / "CLAUDE.md"
original = "# My project\n\n@AGENTS.md\n"
claude_md.write_text(original, encoding="utf-8")
config_body = f"""
[playbook]
project_root = "{tmp_dir}"
deploy_root = "{CUSTOM_DEPLOY_ROOT}"
[sync_memory_bank]
project_name = "Demo"
"""
config_path = Path(tmp_dir) / "playbook.toml"
config_path.write_text(config_body, encoding="utf-8")
result = run_cli("-config", str(config_path))
self.assertEqual(result.returncode, 0)
self.assertEqual(claude_md.read_text(encoding="utf-8"), original)
def test_install_skills_creates_symlink_when_skill_link_configured(self):
with tempfile.TemporaryDirectory() as tmp_dir:
root = Path(tmp_dir)
agents_home = root / "agents"
link_home = root / "claude"
config_body = f"""
[playbook]
project_root = "{tmp_dir}"
deploy_root = "{CUSTOM_DEPLOY_ROOT}"
[install_skills]
agents_home = "{agents_home}"
skill_link = "{link_home}"
mode = "list"
skills = ["commit-message"]
"""
config_path = root / "playbook.toml"
config_path.write_text(config_body, encoding="utf-8")
result = run_cli("-config", str(config_path))
self.assertEqual(result.returncode, 0, msg=result.stdout + result.stderr)
skills_dst = agents_home / "skills"
link_path = link_home / "skills"
self.assertTrue(skills_dst.is_dir())
self.assertTrue(link_path.is_dir(), "link_path should be accessible as dir")
self.assertEqual(link_path.resolve(), skills_dst.resolve())
self.assertTrue((link_path / "commit-message" / "SKILL.md").is_file())
def test_install_skills_symlink_is_idempotent(self):
with tempfile.TemporaryDirectory() as tmp_dir:
root = Path(tmp_dir)
agents_home = root / "agents"
link_home = root / "claude"
config_body = f"""
[playbook]
project_root = "{tmp_dir}"
deploy_root = "{CUSTOM_DEPLOY_ROOT}"
[install_skills]
agents_home = "{agents_home}"
skill_link = "{link_home}"
mode = "list"
skills = ["commit-message"]
no_backup = true
"""
config_path = root / "playbook.toml"
config_path.write_text(config_body, encoding="utf-8")
run_cli("-config", str(config_path))
result = run_cli("-config", str(config_path))
self.assertEqual(result.returncode, 0, msg=result.stdout + result.stderr)
self.assertTrue((link_home / "skills").is_dir())
def test_install_skills_no_symlink_when_skill_link_absent(self):
with tempfile.TemporaryDirectory() as tmp_dir:
root = Path(tmp_dir)
agents_home = root / "agents"
config_body = f"""
[playbook]
project_root = "{tmp_dir}"
deploy_root = "{CUSTOM_DEPLOY_ROOT}"
[install_skills]
agents_home = "{agents_home}"
mode = "list"
skills = ["commit-message"]
"""
config_path = root / "playbook.toml"
config_path.write_text(config_body, encoding="utf-8")
result = run_cli("-config", str(config_path))
self.assertEqual(result.returncode, 0, msg=result.stdout + result.stderr)
self.assertFalse(any(
p.is_symlink() for p in (agents_home / "skills").iterdir()
if p.is_symlink()
) if (agents_home / "skills").exists() else False)
if __name__ == "__main__":
unittest.main()
@@ -79,7 +79,7 @@ echo ""
echo "🔍 验证 memory-bank 模板"
MEMORY_BANK_DIR="$TEMPLATES_DIR/memory-bank"
for name in project-brief tech-stack architecture progress decisions; do
for name in project-brief tech-context system-patterns active-context progress decisions; do
validate_file_exists "$MEMORY_BANK_DIR/$name.template.md" "memory-bank/$name.template.md"
done
@@ -90,9 +90,10 @@ PROMPTS_DIR="$TEMPLATES_DIR/prompts"
validate_file_exists "$PROMPTS_DIR/README.md" "prompts/README.md"
validate_file_exists "$PROMPTS_DIR/system/agent-behavior.template.md" "prompts/system/agent-behavior.template.md"
validate_file_exists "$PROMPTS_DIR/coding/clarify.template.md" "prompts/coding/clarify.template.md"
validate_file_exists "$PROMPTS_DIR/coding/review.template.md" "prompts/coding/review.template.md"
validate_file_exists "$PROMPTS_DIR/coding/verify-change.template.md" "prompts/coding/verify-change.template.md"
validate_file_exists "$PROMPTS_DIR/coding/close-task.template.md" "prompts/coding/close-task.template.md"
validate_file_exists "$PROMPTS_DIR/coding/update-memory.template.md" "prompts/coding/update-memory.template.md"
validate_file_exists "$PROMPTS_DIR/coding/code-review.template.md" "prompts/coding/code-review.template.md"
validate_file_exists "$PROMPTS_DIR/meta/prompt-generator.template.md" "prompts/meta/prompt-generator.template.md"
echo ""
@@ -53,17 +53,21 @@ validate_toml_syntax() {
if python3 << EOF
import sys
try:
import tomli
except ImportError:
parser = None
for module_name in ("tomli", "tomllib", "toml"):
try:
import tomllib as tomli
parser = __import__(module_name)
break
except ImportError:
import toml as tomli
continue
if parser is None:
print("缺少 TOML 解析器(需要 tomli、tomllib 或 toml", file=sys.stderr)
sys.exit(2)
try:
with open("$file", "rb") as f:
tomli.load(f)
parser.load(f)
sys.exit(0)
except Exception as e:
print(f"TOML 语法错误: {e}", file=sys.stderr)
@@ -74,8 +78,14 @@ EOF
VALIDATION_PASSED=$((VALIDATION_PASSED + 1))
return 0
else
echo "$description: TOML 语法错误"
echo "TOML 语法错误: $file" >> "$ERRORS_FILE"
status=$?
if [ "$status" -eq 2 ]; then
echo "$description: 缺少 TOML 解析器"
echo "缺少 TOML 解析器: $file" >> "$ERRORS_FILE"
else
echo "$description: TOML 语法错误"
echo "TOML 语法错误: $file" >> "$ERRORS_FILE"
fi
VALIDATION_FAILED=$((VALIDATION_FAILED + 1))
return 1
fi
@@ -0,0 +1,185 @@
import shutil
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
DEFAULT_DEPLOY_ROOT = Path("docs/standards/playbook")
CUSTOM_DEPLOY_ROOT = Path("custom/playbook")
REPO_COPY_IGNORE = shutil.ignore_patterns(
".git",
".venv",
"node_modules",
"__pycache__",
".pytest_cache",
".mypy_cache",
".ruff_cache",
)
def run_script(
script: Path, *args: str, cwd: Path | None = None
) -> subprocess.CompletedProcess[str]:
return subprocess.run(
[sys.executable, str(script), *args],
capture_output=True,
text=True,
cwd=cwd,
)
def write_config(root: Path, name: str, body: str) -> Path:
config_path = root / name
config_path.write_text(body.strip() + "\n", encoding="utf-8")
return config_path
def copy_repo(target: Path) -> Path:
shutil.copytree(ROOT, target, ignore=REPO_COPY_IGNORE)
return target
class DeploymentRoutesE2ETests(unittest.TestCase):
def assert_core_project_files(self, project_root: Path) -> None:
expected = [
"AGENTS.md",
"AGENT_RULES.md",
"AGENT_RULES.local.md",
"memory-bank/progress.md",
"docs/prompts/system/agent-behavior.md",
".agents/index.md",
".agents/tsl/index.md",
".agents/markdown/index.md",
".gitattributes",
]
for rel in expected:
with self.subTest(path=rel):
self.assertTrue((project_root / rel).exists(), f"missing: {rel}")
def assert_docs_prefix(self, project_root: Path, docs_prefix: str) -> None:
agents_index = (project_root / ".agents" / "index.md").read_text(encoding="utf-8")
self.assertIn("`.agents/tsl/index.md`", agents_index)
self.assertIn("`.agents/markdown/index.md`", agents_index)
self.assertIn(f"- {docs_prefix}", agents_index)
tsl_index = (project_root / ".agents" / "tsl" / "index.md").read_text(
encoding="utf-8"
)
self.assertIn(f"`{docs_prefix}/tsl/index.md`", tsl_index)
self.assertNotIn("`docs/tsl/index.md`", tsl_index)
def test_subtree_style_deployment_syncs_project_files(self):
with tempfile.TemporaryDirectory() as tmp_dir:
tmp_root = Path(tmp_dir)
project_root = tmp_root / "project"
playbook_root = project_root / DEFAULT_DEPLOY_ROOT
project_root.mkdir()
copy_repo(playbook_root)
config_path = write_config(
project_root,
"playbook.toml",
"""
[playbook]
project_root = "."
[sync_rules]
no_backup = true
[sync_memory_bank]
project_name = "Demo"
[sync_prompts]
no_backup = true
[sync_standards]
langs = ["tsl", "markdown"]
no_backup = true
""",
)
result = run_script(
playbook_root / "scripts" / "playbook.py",
"-config",
str(config_path),
cwd=project_root,
)
self.assertEqual(result.returncode, 0, msg=result.stdout + result.stderr)
self.assert_core_project_files(project_root)
self.assert_docs_prefix(project_root, "docs/standards/playbook/docs")
claude_md = project_root / "CLAUDE.md"
self.assertTrue(claude_md.is_file())
text = claude_md.read_text(encoding="utf-8")
self.assertIn("@AGENTS.md", text)
self.assertIn("@AGENT_RULES.md", text)
self.assertIn("<!-- playbook:claude:start -->", text)
def test_external_clone_deployment_vendors_snapshot_and_updates_claude(self):
with tempfile.TemporaryDirectory() as tmp_dir:
tmp_root = Path(tmp_dir)
external_clone = tmp_root / "playbook"
project_root = tmp_root / "project"
copy_repo(external_clone)
project_root.mkdir()
claude_md = project_root / ".claude" / "CLAUDE.md"
claude_md.parent.mkdir()
claude_md.write_text("# Existing Claude\n\nKeep this.\n", encoding="utf-8")
config_path = write_config(
project_root,
"playbook.toml",
f"""
[playbook]
project_root = "."
deploy_root = "{CUSTOM_DEPLOY_ROOT.as_posix()}"
[vendor]
langs = ["tsl", "markdown"]
[sync_rules]
no_backup = true
[sync_memory_bank]
project_name = "Demo"
[sync_prompts]
no_backup = true
[sync_standards]
langs = ["tsl", "markdown"]
no_backup = true
""",
)
result = run_script(
external_clone / "scripts" / "playbook.py",
"-config",
str(config_path),
cwd=project_root,
)
self.assertEqual(result.returncode, 0, msg=result.stdout + result.stderr)
snapshot_root = project_root / CUSTOM_DEPLOY_ROOT
self.assertTrue((snapshot_root / "SOURCE.md").is_file())
self.assertTrue((snapshot_root / "scripts" / "playbook.py").is_file())
self.assertTrue((snapshot_root / "templates" / "README.md").is_file())
self.assert_core_project_files(project_root)
self.assert_docs_prefix(project_root, "custom/playbook/docs")
text = claude_md.read_text(encoding="utf-8")
self.assertIn("Keep this.", text)
self.assertIn("@../AGENTS.md", text)
self.assertIn("@../AGENT_RULES.md", text)
self.assertEqual(text.count("<!-- playbook:claude:start -->"), 1)
self.assertFalse((project_root / "CLAUDE.md").exists())
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,104 @@
import re
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
SKILLS_ROOT = ROOT / "skills"
FIRST_PARTY_SKILLS = {
"commit-message": SKILLS_ROOT / "commit-message" / "SKILL.md",
"style-cleanup": SKILLS_ROOT / "style-cleanup" / "SKILL.md",
"bulk-refactor-workflow": SKILLS_ROOT / "bulk-refactor-workflow" / "SKILL.md",
}
def read_text(path: Path) -> str:
return path.read_text(encoding="utf-8")
def normalize_space(text: str) -> str:
return " ".join(text.split())
def parse_frontmatter(text: str) -> dict[str, str]:
match = re.match(r"^---\n(.*?)\n---\n", text, re.DOTALL)
if match is None:
raise AssertionError("missing YAML frontmatter")
block = match.group(1)
data: dict[str, str] = {}
current_key: str | None = None
current_value: list[str] = []
for raw_line in block.splitlines():
if raw_line.startswith(" ") and current_key is not None:
current_value.append(raw_line.strip())
continue
if current_key is not None:
data[current_key] = " ".join(current_value).strip().strip('"')
current_key = None
current_value = []
key, value = raw_line.split(":", 1)
current_key = key.strip()
current_value = [value.strip()]
if current_key is not None:
data[current_key] = " ".join(current_value).strip().strip('"')
return data
class FirstPartySkillsQualityTests(unittest.TestCase):
def test_first_party_skill_frontmatter_is_minimal_and_named_consistently(self):
for name, path in FIRST_PARTY_SKILLS.items():
with self.subTest(skill=name):
frontmatter = parse_frontmatter(read_text(path))
self.assertEqual(set(frontmatter), {"name", "description"})
self.assertEqual(frontmatter["name"], name)
self.assertRegex(frontmatter["name"], r"^[a-z0-9-]+$")
def test_first_party_skill_descriptions_are_trigger_focused(self):
for name, path in FIRST_PARTY_SKILLS.items():
with self.subTest(skill=name):
description = parse_frontmatter(read_text(path))["description"]
self.assertTrue(description.startswith("Use when"))
self.assertLessEqual(len(description), 500)
self.assertNotIn("Triggers:", description)
def test_first_party_skills_have_required_sections(self):
required_sections = (
"## Overview",
"## When to Use",
"## When Not to Use",
"## Inputs",
"## Procedure",
"## Output Contract",
"## Success Criteria",
"## Failure Handling",
)
for name, path in FIRST_PARTY_SKILLS.items():
text = read_text(path)
with self.subTest(skill=name):
for section in required_sections:
self.assertIn(section, text)
def test_commit_message_skill_handles_missing_or_mixed_staging_states(self):
text = normalize_space(read_text(FIRST_PARTY_SKILLS["commit-message"]))
self.assertIn("If nothing is staged", text)
self.assertIn("If only unstaged changes exist", text)
self.assertIn("strongly recommend splitting the commit", text)
self.assertIn("Do not run `git commit`", text)
def test_style_cleanup_skill_has_clear_non_goals_and_verification_loop(self):
text = normalize_space(read_text(FIRST_PARTY_SKILLS["style-cleanup"]))
self.assertIn("not for semantic refactors", text)
self.assertIn("not for introducing a new formatter or lint configuration", text)
self.assertIn("formatter -> lint/check -> lint --fix -> final check", text)
self.assertIn("second formatter run produces no additional diff", text)
def test_bulk_refactor_skill_is_dirty_aware_and_delegates_final_cleanup(self):
text = normalize_space(read_text(FIRST_PARTY_SKILLS["bulk-refactor-workflow"]))
self.assertIn("Dirty worktrees are allowed", text)
self.assertIn("Do not revert unrelated changes", text)
self.assertIn("Use `style-cleanup` for the final formatting/lint pass", text)
self.assertIn("apply the transformation in bounded batches", text)
if __name__ == "__main__":
unittest.main()
@@ -26,14 +26,22 @@ class FormatMdActionTests(unittest.TestCase):
bin_dir = root / "bin"
bin_dir.mkdir()
prettier = bin_dir / "prettier"
prettier.write_text(
"#!/usr/bin/env python3\n"
"from pathlib import Path\n"
"Path(\".prettier_called\").write_text(\"ok\")\n",
encoding="utf-8",
)
prettier.chmod(0o755)
if os.name == "nt":
prettier = bin_dir / "prettier.cmd"
prettier.write_text(
"@echo off\r\n"
"echo ok> .prettier_called\r\n",
encoding="utf-8",
)
else:
prettier = bin_dir / "prettier"
prettier.write_text(
"#!/usr/bin/env python3\n"
"from pathlib import Path\n"
"Path(\".prettier_called\").write_text(\"ok\")\n",
encoding="utf-8",
)
prettier.chmod(0o755)
config_body = f"""
[playbook]
@@ -47,7 +55,7 @@ project_root = \"{tmp_dir}\"
config_path.write_text(config_body, encoding="utf-8")
env = os.environ.copy()
env["PATH"] = f"{bin_dir}:{env.get('PATH', '')}"
env["PATH"] = f"{bin_dir}{os.pathsep}{env.get('PATH', '')}"
result = run_cli("-config", str(config_path), env=env)
self.assertEqual(result.returncode, 0, msg=result.stderr)
@@ -7,6 +7,7 @@ from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
SCRIPT = ROOT / "scripts" / "playbook.py"
SOURCE_GITATTR = ROOT / ".gitattributes"
DEFAULT_DEPLOY_ROOT = "docs/standards/playbook"
def run_cli(*args, cwd=None):
@@ -33,6 +34,7 @@ class GitattributesModeTests(unittest.TestCase):
config_body = f"""
[playbook]
project_root = \"{root}\"
deploy_root = "{DEFAULT_DEPLOY_ROOT}"
[sync_standards]
langs = [\"tsl\"]
@@ -0,0 +1,49 @@
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
TEST_WORKFLOW = ROOT / ".gitea" / "workflows" / "test.yml"
STANDARDS_WORKFLOW = ROOT / ".gitea" / "workflows" / "standards-check.yml"
UPDATE_THIRDPARTY_WORKFLOW = ROOT / ".gitea" / "workflows" / "update-thirdparty-skills.yml"
TEMPLATE_STANDARDS_WORKFLOW = (
ROOT / "templates" / "ci" / "gitea" / ".gitea" / "workflows" / "standards-check.yml"
)
class GiteaWorkflowBootstrapTests(unittest.TestCase):
def test_repo_workflows_do_not_call_repo_local_prepare_script_before_checkout(self):
for workflow in (TEST_WORKFLOW, STANDARDS_WORKFLOW):
text = workflow.read_text(encoding="utf-8")
with self.subTest(workflow=workflow.name):
self.assertNotIn("bash .gitea/ci/prepare_repo.sh", text)
self.assertIn('git clone "$REPO_URL" "$REPO_DIR"', text)
self.assertIn('echo "REPO_DIR=$REPO_DIR" >> "$GITHUB_ENV"', text)
def test_workflows_use_isolated_repo_dirs_per_job(self):
for workflow in (
TEST_WORKFLOW,
STANDARDS_WORKFLOW,
UPDATE_THIRDPARTY_WORKFLOW,
TEMPLATE_STANDARDS_WORKFLOW,
):
text = workflow.read_text(encoding="utf-8")
with self.subTest(workflow=workflow.name):
self.assertNotIn('REPO_DIR="${WORKSPACE_DIR}/${REPO_NAME}"', text)
self.assertNotIn('REPO_DIR="${{ env.WORKSPACE_DIR }}/$REPO_NAME"', text)
self.assertIn('mktemp -d', text)
self.assertTrue(
'mkdir -p "$WORKSPACE_DIR"' in text
or 'mkdir -p "${{ env.WORKSPACE_DIR }}"' in text
)
self.assertIn('echo "REPO_DIR=$REPO_DIR" >>', text)
self.assertIn("if: always()", text)
self.assertIn('rm -rf "$REPO_DIR"', text)
def test_test_workflow_installs_tomli_for_python_template_validation(self):
text = TEST_WORKFLOW.read_text(encoding="utf-8")
self.assertIn("python3 -m pip install yamllint tomli", text)
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,584 @@
import importlib.util
import os
import platform
import subprocess
import sys
import tempfile
import threading
import time
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
SCRIPT = ROOT / "scripts" / "main_loop.py"
_SPEC = importlib.util.spec_from_file_location("playbook_main_loop", SCRIPT)
assert _SPEC and _SPEC.loader
MAIN_LOOP = importlib.util.module_from_spec(_SPEC)
_SPEC.loader.exec_module(MAIN_LOOP)
def run_cli(*args, cwd=None):
return subprocess.run(
[sys.executable, str(SCRIPT), *args],
capture_output=True,
text=True,
cwd=cwd,
)
class MainLoopCliTests(unittest.TestCase):
def _current_env(self) -> str:
system = platform.system().lower()
mapping = {"windows": "windows", "linux": "linux", "darwin": "darwin"}
if system not in mapping:
self.skipTest(f"Unsupported environment: {system}")
return mapping[system]
def test_claim_seeds_progress_and_marks_first_plan_in_progress(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-old.md").write_text("old", encoding="utf-8")
(plans_dir / "2026-01-02-new.md").write_text("new", 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-01-old.md",
)
progress = root / "memory-bank" / "progress.md"
text = progress.read_text(encoding="utf-8")
self.assertIn("<!-- workflow-state:start -->", 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:end -->", text)
self.assertIn("`2026-01-01-old.md` in-progress", text)
self.assertIn("`2026-01-02-new.md` pending", text)
def test_claim_preserves_human_progress_sections_when_plan_block_missing(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-demo.md").write_text("demo", encoding="utf-8")
progress = root / "memory-bank" / "progress.md"
progress.parent.mkdir(parents=True)
progress.write_text(
"\n".join(
[
"# 当前进展",
"",
"## Current Focus",
"",
"- keep-this-focus",
"",
"## Recent Changes",
"",
"- keep-this-change",
"",
]
)
+ "\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)
text = progress.read_text(encoding="utf-8")
self.assertIn("- keep-this-focus", text)
self.assertIn("- keep-this-change", text)
self.assertIn("<!-- workflow-state:start -->", text)
self.assertIn("<!-- plan-status:start -->", text)
self.assertIn("`2026-01-01-demo.md` in-progress", text)
def test_claim_returns_existing_in_progress_before_pending(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-a.md").write_text("a", encoding="utf-8")
(plans_dir / "2026-01-02-b.md").write_text("b", encoding="utf-8")
progress = root / "memory-bank" / "progress.md"
progress.parent.mkdir(parents=True)
progress.write_text(
"\n".join(
[
"# Plan 状态",
"",
"<!-- plan-status:start -->",
"- [ ] `2026-01-02-b.md` pending",
"- [ ] `2026-01-01-a.md` in-progress",
"<!-- plan-status:end -->",
"",
]
),
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-01-a.md",
)
def test_claim_skips_stale_progress_entries_for_deleted_plans(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-02-live.md").write_text("live", encoding="utf-8")
progress = root / "memory-bank" / "progress.md"
progress.parent.mkdir(parents=True)
progress.write_text(
"\n".join(
[
"# Plan 状态",
"",
"<!-- workflow-state:start -->",
"phase: planning",
"<!-- workflow-state:end -->",
"",
"<!-- plan-status:start -->",
"- [ ] `2026-01-01-deleted.md` pending",
"- [ ] `2026-01-02-live.md` pending",
"<!-- plan-status:end -->",
"",
]
),
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-live.md",
)
def test_claim_resumes_env_blocked_plan_and_preserves_note(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-05-env.md").write_text("env", encoding="utf-8")
progress = root / "memory-bank" / "progress.md"
progress.parent.mkdir(parents=True)
env = self._current_env()
note = f"env:{env}:Task1,Task3"
progress.write_text(
"\n".join(
[
"# Plan 状态",
"",
"<!-- plan-status:start -->",
f"- [ ] `2026-01-05-env.md` blocked: {note}",
"<!-- plan-status:end -->",
"",
]
),
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(),
"\n".join(
[
"PLAN=docs/superpowers/plans/2026-01-05-env.md",
f"NOTE={note}",
]
),
)
text = progress.read_text(encoding="utf-8")
self.assertIn(f"`2026-01-05-env.md` in-progress: {note}", text)
def test_finish_updates_line(self):
with tempfile.TemporaryDirectory() as tmp_dir:
root = Path(tmp_dir)
progress = root / "memory-bank" / "progress.md"
progress.parent.mkdir(parents=True)
progress.write_text(
"\n".join(
[
"# Plan 状态",
"",
"<!-- plan-status:start -->",
"- [ ] `2026-01-03-demo.md` in-progress",
"<!-- plan-status:end -->",
"",
]
),
encoding="utf-8",
)
result = run_cli(
"finish",
"-plan",
"docs/superpowers/plans/2026-01-03-demo.md",
"-status",
"done",
"-progress",
"memory-bank/progress.md",
cwd=root,
)
self.assertEqual(result.returncode, 0, msg=result.stderr)
text = progress.read_text(encoding="utf-8")
self.assertIn("- [x] `2026-01-03-demo.md` done", text)
self.assertEqual(
text.count("- [x] `2026-01-03-demo.md` done"),
1,
)
def test_finish_updates_workflow_phase_and_preserves_metadata(self):
with tempfile.TemporaryDirectory() as tmp_dir:
root = Path(tmp_dir)
progress = root / "memory-bank" / "progress.md"
progress.parent.mkdir(parents=True)
progress.write_text(
"\n".join(
[
"# 当前进展",
"",
"## Workflow State",
"",
"<!-- workflow-state:start -->",
"phase: executing",
"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:start -->",
"- [ ] `2026-05-18-demo.md` in-progress",
"<!-- plan-status:end -->",
"",
]
)
+ "\n",
encoding="utf-8",
)
result = run_cli(
"finish",
"-plan",
"docs/superpowers/plans/2026-05-18-demo.md",
"-status",
"done",
"-progress",
"memory-bank/progress.md",
cwd=root,
)
self.assertEqual(result.returncode, 0, msg=result.stderr)
text = progress.read_text(encoding="utf-8")
self.assertIn("phase: done", text)
self.assertIn("spec: docs/superpowers/specs/2026-05-18-demo-design.md", text)
self.assertIn("executor: executing-plans", text)
self.assertIn(
"constraints: karpathy-guidelines,.agents,AGENT_RULES",
text,
)
def test_record_updates_workflow_state_block(self):
with tempfile.TemporaryDirectory() as tmp_dir:
root = Path(tmp_dir)
progress = root / "memory-bank" / "progress.md"
progress.parent.mkdir(parents=True)
progress.write_text(
"\n".join(
[
"# 当前进展",
"",
"## Plan Status",
"",
"<!-- plan-status:start -->",
"<!-- plan-status:end -->",
"",
]
)
+ "\n",
encoding="utf-8",
)
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)
text = progress.read_text(encoding="utf-8")
self.assertIn("<!-- workflow-state:start -->", 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):
with tempfile.TemporaryDirectory() as tmp_dir:
root = Path(tmp_dir)
plans_dir = root / "docs" / "superpowers" / "plans"
plans_dir.mkdir(parents=True)
(plans_dir / "2026-05-18-demo.md").write_text("demo", encoding="utf-8")
progress = root / "memory-bank" / "progress.md"
progress.parent.mkdir(parents=True)
result = run_cli(
"record",
"-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)
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(
"claim",
"-plans",
"docs/superpowers/plans",
"-progress",
"memory-bank/progress.md",
cwd=root,
)
self.assertEqual(result.returncode, 0, msg=result.stderr)
result = run_cli(
"finish",
"-plan",
"docs/superpowers/plans/2026-05-18-demo.md",
"-status",
"done",
"-progress",
"memory-bank/progress.md",
cwd=root,
)
self.assertEqual(result.returncode, 0, msg=result.stderr)
text = progress.read_text(encoding="utf-8")
self.assertIn("phase: done", 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)
def test_concurrent_record_preserves_spec_and_plan_metadata(self):
with tempfile.TemporaryDirectory() as tmp_dir:
root = Path(tmp_dir)
progress = root / "memory-bank" / "progress.md"
progress.parent.mkdir(parents=True)
original_load = MAIN_LOOP.load_progress_lines
first_load = {"seen": False}
gate = threading.Lock()
def delayed_load(progress_path):
lines = original_load(progress_path)
with gate:
if not first_load["seen"]:
first_load["seen"] = True
threading.Event().wait(0.2)
return lines
MAIN_LOOP.load_progress_lines = delayed_load
try:
threads = [
threading.Thread(
target=MAIN_LOOP.record_workflow_state,
args=(
progress,
"planning",
"docs/superpowers/specs/2026-05-18-demo-design.md",
None,
None,
None,
),
),
threading.Thread(
target=MAIN_LOOP.record_workflow_state,
args=(
progress,
"planning",
None,
"docs/superpowers/plans/2026-05-18-demo.md",
"executing-plans",
"karpathy-guidelines,.agents,AGENT_RULES",
),
),
]
for thread in threads:
thread.start()
for thread in threads:
thread.join()
finally:
MAIN_LOOP.load_progress_lines = original_load
text = progress.read_text(encoding="utf-8")
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_cross_process_record_lock_preserves_spec_and_plan_metadata(self):
with tempfile.TemporaryDirectory() as tmp_dir:
root = Path(tmp_dir)
progress = root / "memory-bank" / "progress.md"
progress.parent.mkdir(parents=True)
slow_env = dict(os.environ)
slow_env["PLAYBOOK_MAIN_LOOP_HOLD_LOCK_MS"] = "300"
proc = subprocess.Popen(
[
sys.executable,
str(SCRIPT),
"record",
"-progress",
"memory-bank/progress.md",
"-phase",
"planning",
"-spec",
"docs/superpowers/specs/2026-05-18-demo-design.md",
],
cwd=root,
env=slow_env,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
time.sleep(0.05)
result = run_cli(
"record",
"-progress",
"memory-bank/progress.md",
"-phase",
"planning",
"-plan",
"docs/superpowers/plans/2026-05-18-demo.md",
"-executor",
"executing-plans",
"-constraints",
"karpathy-guidelines,.agents,AGENT_RULES",
cwd=root,
)
stdout, stderr = proc.communicate(timeout=5)
self.assertEqual(proc.returncode, 0, msg=stderr or stdout)
self.assertEqual(result.returncode, 0, msg=result.stderr)
text = progress.read_text(encoding="utf-8")
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,
)
if __name__ == "__main__":
unittest.main()
@@ -6,6 +6,7 @@ from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
SCRIPT = ROOT / "scripts" / "playbook.py"
DEFAULT_DEPLOY_ROOT = "docs/standards/playbook"
def run_cli(*args):
@@ -26,6 +27,7 @@ class NoBackupFlagsTests(unittest.TestCase):
config_body = f"""
[playbook]
project_root = "{tmp_dir}"
deploy_root = "{DEFAULT_DEPLOY_ROOT}"
[sync_rules]
force = true
@@ -54,6 +56,7 @@ no_backup = true
config_body = f"""
[playbook]
project_root = "{tmp_dir}"
deploy_root = "{DEFAULT_DEPLOY_ROOT}"
[sync_standards]
langs = ["tsl"]
@@ -71,6 +74,36 @@ no_backup = true
git_backups = list(root.glob(".gitattributes.bak.*"))
self.assertEqual(git_backups, [])
def test_install_skills_no_backup_replaces_existing_skill_without_backup(self):
with tempfile.TemporaryDirectory() as tmp_dir:
root = Path(tmp_dir)
skills_root = root / "agents" / "skills"
existing = skills_root / "brainstorming"
existing.mkdir(parents=True)
(existing / "stale.txt").write_text("old", encoding="utf-8")
config_body = f"""
[playbook]
project_root = "{tmp_dir}"
deploy_root = "{DEFAULT_DEPLOY_ROOT}"
[install_skills]
agents_home = "{root / 'agents'}"
mode = "list"
skills = ["brainstorming"]
no_backup = true
"""
config_path = root / "playbook.toml"
config_path.write_text(config_body, encoding="utf-8")
result = run_cli("-config", str(config_path))
self.assertEqual(result.returncode, 0, msg=result.stderr)
backups = list(skills_root.glob("brainstorming.bak.*"))
self.assertEqual(backups, [])
self.assertFalse((existing / "stale.txt").exists())
self.assertTrue((existing / "SKILL.md").is_file())
if __name__ == "__main__":
unittest.main()
@@ -1,166 +0,0 @@
import subprocess
import sys
import tempfile
import unittest
import platform
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
SCRIPT = ROOT / "scripts" / "plan_progress.py"
def run_cli(*args, cwd=None):
return subprocess.run(
[sys.executable, str(SCRIPT), *args],
capture_output=True,
text=True,
cwd=cwd,
)
class PlanProgressCliTests(unittest.TestCase):
def _current_env(self) -> str:
system = platform.system().lower()
mapping = {"windows": "windows", "linux": "linux", "darwin": "darwin"}
if system not in mapping:
self.skipTest(f"Unsupported environment: {system}")
return mapping[system]
def test_select_seeds_progress_when_missing(self):
with tempfile.TemporaryDirectory() as tmp_dir:
root = Path(tmp_dir)
plans_dir = root / "docs" / "plans"
plans_dir.mkdir(parents=True)
(plans_dir / "2026-01-01-old.md").write_text("old", encoding="utf-8")
(plans_dir / "2026-01-02-new.md").write_text("new", encoding="utf-8")
result = run_cli(
"select",
"-plans",
"docs/plans",
"-progress",
"memory-bank/progress.md",
cwd=root,
)
self.assertEqual(result.returncode, 0, msg=result.stderr)
self.assertEqual(result.stdout.strip(), "docs/plans/2026-01-01-old.md")
progress = root / "memory-bank" / "progress.md"
text = progress.read_text(encoding="utf-8")
self.assertIn("<!-- plan-status:start -->", text)
self.assertIn("<!-- plan-status:end -->", text)
self.assertIn("`2026-01-01-old.md` pending", text)
self.assertIn("`2026-01-02-new.md` pending", text)
def test_select_returns_first_pending_in_order(self):
with tempfile.TemporaryDirectory() as tmp_dir:
root = Path(tmp_dir)
plans_dir = root / "docs" / "plans"
plans_dir.mkdir(parents=True)
(plans_dir / "2026-01-01-a.md").write_text("a", encoding="utf-8")
(plans_dir / "2026-01-02-b.md").write_text("b", encoding="utf-8")
progress = root / "memory-bank" / "progress.md"
progress.parent.mkdir(parents=True)
progress.write_text(
"\n".join(
[
"# Plan 状态",
"",
"<!-- plan-status:start -->",
"- [ ] `2026-01-02-b.md` pending",
"- [ ] `2026-01-01-a.md` pending",
"<!-- plan-status:end -->",
"",
]
),
encoding="utf-8",
)
result = run_cli(
"select",
"-plans",
"docs/plans",
"-progress",
"memory-bank/progress.md",
cwd=root,
)
self.assertEqual(result.returncode, 0, msg=result.stderr)
self.assertEqual(result.stdout.strip(), "docs/plans/2026-01-02-b.md")
def test_select_returns_env_blocked_plan_without_flag(self):
with tempfile.TemporaryDirectory() as tmp_dir:
root = Path(tmp_dir)
plans_dir = root / "docs" / "plans"
plans_dir.mkdir(parents=True)
(plans_dir / "2026-01-05-env.md").write_text("env", encoding="utf-8")
progress = root / "memory-bank" / "progress.md"
progress.parent.mkdir(parents=True)
env = self._current_env()
progress.write_text(
"\n".join(
[
"# Plan 状态",
"",
"<!-- plan-status:start -->",
f"- [ ] `2026-01-05-env.md` blocked: env:{env}:Task1",
"<!-- plan-status:end -->",
"",
]
),
encoding="utf-8",
)
result = run_cli(
"select",
"-plans",
"docs/plans",
"-progress",
"memory-bank/progress.md",
cwd=root,
)
self.assertEqual(result.returncode, 0, msg=result.stderr)
self.assertEqual(result.stdout.strip(), "docs/plans/2026-01-05-env.md")
def test_record_updates_line(self):
with tempfile.TemporaryDirectory() as tmp_dir:
root = Path(tmp_dir)
progress = root / "memory-bank" / "progress.md"
progress.parent.mkdir(parents=True)
progress.write_text(
"\n".join(
[
"# Plan 状态",
"",
"<!-- plan-status:start -->",
"- [ ] `2026-01-03-demo.md` pending",
"<!-- plan-status:end -->",
"",
]
),
encoding="utf-8",
)
result = run_cli(
"record",
"-plan",
"docs/plans/2026-01-03-demo.md",
"-status",
"done",
"-progress",
"memory-bank/progress.md",
cwd=root,
)
self.assertEqual(result.returncode, 0, msg=result.stderr)
text = progress.read_text(encoding="utf-8")
self.assertIn("- [x] `2026-01-03-demo.md` done", text)
self.assertEqual(text.count("2026-01-03-demo.md"), 1)
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,61 @@
import importlib.util
import tempfile
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
SCRIPT = ROOT / "scripts" / "playbook.py"
def load_playbook_module():
spec = importlib.util.spec_from_file_location("playbook_script", SCRIPT)
module = importlib.util.module_from_spec(spec)
assert spec.loader is not None
spec.loader.exec_module(module)
return module
class PlaybookDocsIndexTests(unittest.TestCase):
def test_build_docs_index_lines_uses_canonical_sections(self):
playbook = load_playbook_module()
with tempfile.TemporaryDirectory() as tmp_dir:
source = Path(tmp_dir) / "index.md"
source.write_text(
"\n".join(
[
"# 文档导航(Docs Index",
"",
"仓库级说明。",
"",
"## 跨语言(common",
"",
"- 公共入口:`common/commit_message.md`",
"",
"## TSLtsl/tsf",
"",
"- 自定义 TSL 入口:`tsl/custom.md`",
"",
"## Pythonpython",
"",
"- Python 入口:`python/style_guide.md`",
"",
]
)
+ "\n",
encoding="utf-8",
)
lines = playbook.build_docs_index_lines(["tsl"], source)
self.assertEqual(lines[0], "# 文档导航(Docs Index")
self.assertEqual(lines[2], "本快照为裁剪版 Playbooklangs: tsl)。")
self.assertIn("## 跨语言(common", lines)
self.assertIn("- 公共入口:`common/commit_message.md`", lines)
self.assertIn("## TSLtsl/tsf", lines)
self.assertIn("- 自定义 TSL 入口:`tsl/custom.md`", lines)
self.assertNotIn("## Pythonpython", lines)
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,32 @@
import ast
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
PLAYBOOK_SCRIPT = ROOT / "scripts" / "playbook.py"
class PlaybookTypingImportTests(unittest.TestCase):
def test_optional_annotation_names_are_imported(self):
tree = ast.parse(PLAYBOOK_SCRIPT.read_text(encoding="utf-8"))
imported_names: set[str] = set()
referenced_names: set[str] = set()
for node in ast.walk(tree):
if isinstance(node, ast.Import):
for alias in node.names:
imported_names.add(alias.asname or alias.name.split(".")[0])
elif isinstance(node, ast.ImportFrom):
for alias in node.names:
imported_names.add(alias.asname or alias.name)
elif isinstance(node, ast.Name):
referenced_names.add(node.id)
if "Optional" in referenced_names:
self.assertIn("Optional", imported_names)
if __name__ == "__main__":
unittest.main()
@@ -1,42 +0,0 @@
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
SKILLS_MD = ROOT / "SKILLS.md"
SOURCES_LIST = ROOT / "codex" / "skills" / ".sources" / "superpowers.list"
def read_sources_list() -> list[str]:
return [
line.strip()
for line in SOURCES_LIST.read_text(encoding="utf-8").splitlines()
if line.strip() and not line.strip().startswith("#")
]
def read_skills_md_list() -> list[str]:
lines = SKILLS_MD.read_text(encoding="utf-8").splitlines()
start = "<!-- superpowers:skills:start -->"
end = "<!-- superpowers:skills:end -->"
try:
start_idx = lines.index(start) + 1
end_idx = lines.index(end)
except ValueError as exc:
raise AssertionError("superpowers markers missing in SKILLS.md") from exc
items = []
for line in lines[start_idx:end_idx]:
stripped = line.strip()
if not stripped.startswith("-"):
continue
items.append(stripped.lstrip("- ").strip())
return items
class SuperpowersListSyncTests(unittest.TestCase):
def test_superpowers_list_matches_skills_md(self):
self.assertEqual(read_sources_list(), read_skills_md_list())
if __name__ == "__main__":
unittest.main()
@@ -1,56 +0,0 @@
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
SYNC_WORKFLOW = ROOT / ".gitea" / "workflows" / "sync-superpowers.yml"
AUTO_UPDATE_WORKFLOW = ROOT / ".gitea" / "workflows" / "update-thirdparty-superpowers.yml"
AUTO_UPDATE_SCRIPT = ROOT / ".gitea" / "ci" / "update_thirdparty_superpowers.sh"
SYNC_SCRIPT = ROOT / ".gitea" / "ci" / "sync_superpowers.sh"
class SuperpowersWorkflowTests(unittest.TestCase):
def test_sync_workflow_uses_manual_trigger(self):
text = SYNC_WORKFLOW.read_text(encoding="utf-8")
self.assertIn("workflow_dispatch:", text)
def test_sync_workflow_runs_from_latest_main(self):
text = SYNC_WORKFLOW.read_text(encoding="utf-8")
self.assertIn('TARGET_BRANCH: "main"', text)
self.assertIn('git fetch origin "${{ env.TARGET_BRANCH }}"', text)
self.assertIn(
'git checkout -B "${{ env.TARGET_BRANCH }}" "origin/${{ env.TARGET_BRANCH }}"',
text,
)
def test_auto_update_workflow_triggers_on_main_push(self):
text = AUTO_UPDATE_WORKFLOW.read_text(encoding="utf-8")
self.assertIn("push:", text)
self.assertIn("- main", text)
self.assertIn("workflow_dispatch:", text)
def test_auto_update_workflow_runs_update_script(self):
text = AUTO_UPDATE_WORKFLOW.read_text(encoding="utf-8")
self.assertIn("bash .gitea/ci/update_thirdparty_superpowers.sh", text)
def test_auto_update_script_targets_thirdparty_branch(self):
text = AUTO_UPDATE_SCRIPT.read_text(encoding="utf-8")
self.assertIn('TARGET_BRANCH="${TARGET_BRANCH:-thirdparty/skill}"', text)
self.assertIn("api.github.com/repos", text)
self.assertIn("ls-remote", text)
self.assertIn('git checkout -B "$TARGET_BRANCH" "origin/$TARGET_BRANCH"', text)
def test_ci_scripts_use_ci_bot_identity(self):
sync_text = SYNC_SCRIPT.read_text(encoding="utf-8")
update_text = AUTO_UPDATE_SCRIPT.read_text(encoding="utf-8")
self.assertIn('COMMIT_AUTHOR_NAME="${COMMIT_AUTHOR_NAME:-ci-bot}"', sync_text)
self.assertIn('COMMIT_AUTHOR_EMAIL="${COMMIT_AUTHOR_EMAIL:-ci-bot@local}"', sync_text)
self.assertIn('COMMIT_AUTHOR_NAME="${COMMIT_AUTHOR_NAME:-ci-bot}"', update_text)
self.assertIn(
'COMMIT_AUTHOR_EMAIL="${COMMIT_AUTHOR_EMAIL:-ci-bot@local}"',
update_text,
)
if __name__ == "__main__":
unittest.main()
@@ -6,6 +6,7 @@ from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
SCRIPT = ROOT / "scripts" / "playbook.py"
DEFAULT_DEPLOY_ROOT = "docs/standards/playbook"
def run_cli(*args):
@@ -28,6 +29,7 @@ class SyncDirectoryActionsTests(unittest.TestCase):
config_body = f"""
[playbook]
project_root = "{tmp_dir}"
deploy_root = "{DEFAULT_DEPLOY_ROOT}"
[sync_memory_bank]
project_name = "Demo"
@@ -52,6 +54,7 @@ project_name = "Demo"
config_body = f"""
[playbook]
project_root = "{tmp_dir}"
deploy_root = "{DEFAULT_DEPLOY_ROOT}"
[sync_prompts]
"""
@@ -78,6 +81,7 @@ project_root = "{tmp_dir}"
config_body = f"""
[playbook]
project_root = "{tmp_dir}"
deploy_root = "{DEFAULT_DEPLOY_ROOT}"
[sync_memory_bank]
project_name = "Demo"
@@ -6,6 +6,7 @@ from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
SCRIPT = ROOT / "scripts" / "playbook.py"
DEFAULT_DEPLOY_ROOT = "docs/standards/playbook"
def run_cli(*args):
@@ -26,14 +27,69 @@ def run_script(script_path: Path, *args, cwd: Path | None = None):
class SyncTemplatesPlaceholdersTests(unittest.TestCase):
def test_main_language_placeholder_replaced(self):
def test_templates_no_longer_expose_main_language_placeholder(self):
example_text = (ROOT / "playbook.toml.example").read_text(encoding="utf-8")
self.assertNotIn("main_language", example_text)
templates_readme = (ROOT / "templates" / "README.md").read_text(
encoding="utf-8"
)
self.assertNotIn("{{MAIN_LANGUAGE}}", templates_readme)
self.assertNotIn("{{LANGUAGE_1}}", templates_readme)
agents_template = (ROOT / "templates" / "AGENTS.template.md").read_text(
encoding="utf-8"
)
self.assertNotIn("{{MAIN_LANGUAGE}}", agents_template)
tech_context_template = (
ROOT / "templates" / "memory-bank" / "tech-context.template.md"
).read_text(encoding="utf-8")
self.assertNotIn("{{MAIN_LANGUAGE}}", tech_context_template)
self.assertNotIn("{{LANGUAGE_1}}", tech_context_template)
self.assertNotIn("**主要语言**", tech_context_template)
update_memory_template = (
ROOT / "templates" / "prompts" / "coding" / "update-memory.template.md"
).read_text(encoding="utf-8")
self.assertIn("workflow-state", update_memory_template)
self.assertIn("plan-status", update_memory_template)
close_task_template = (
ROOT / "templates" / "prompts" / "coding" / "close-task.template.md"
).read_text(encoding="utf-8")
self.assertIn("main_loop.py finish", close_task_template)
self.assertIn("workflow-state.phase", close_task_template)
verify_change_template = (
ROOT / "templates" / "prompts" / "coding" / "verify-change.template.md"
).read_text(encoding="utf-8")
self.assertIn("workflow-state.phase", verify_change_template)
self.assertIn("plan-status", verify_change_template)
prompts_readme = (
ROOT / "templates" / "prompts" / "README.md"
).read_text(encoding="utf-8")
self.assertIn("playbook.py -record-spec", prompts_readme)
self.assertIn("playbook.py -record-plan", prompts_readme)
agent_behavior_template = (
ROOT / "templates" / "prompts" / "system" / "agent-behavior.template.md"
).read_text(encoding="utf-8")
self.assertIn("playbook.py -record-spec", agent_behavior_template)
self.assertIn("playbook.py -record-plan", agent_behavior_template)
def test_sync_templates_replaces_playbook_scripts_without_main_language_support(self):
with tempfile.TemporaryDirectory() as tmp_dir:
config_body = f"""
[playbook]
project_root = \"{tmp_dir}\"
deploy_root = "{DEFAULT_DEPLOY_ROOT}"
[sync_rules]
[sync_memory_bank]
[sync_standards]
langs = [\"cpp\", \"tsl\"]
"""
@@ -48,10 +104,30 @@ langs = [\"cpp\", \"tsl\"]
self.assertIn(".agents/cpp/index.md", text)
self.assertNotIn("{{MAIN_LANGUAGE}}", text)
tech_context = Path(tmp_dir) / "memory-bank" / "tech-context.md"
tech_context_text = tech_context.read_text(encoding="utf-8")
self.assertNotIn("{{LANGUAGE_1}}", tech_context_text)
self.assertNotIn("{{MAIN_LANGUAGE}}", tech_context_text)
self.assertNotIn("**主要语言**", tech_context_text)
rules_md = Path(tmp_dir) / "AGENT_RULES.md"
rules_text = rules_md.read_text(encoding="utf-8")
self.assertIn("docs/standards/playbook/scripts/plan_progress.py", rules_text)
self.assertIn(
"docs/standards/playbook/scripts/main_loop.py claim",
rules_text,
)
self.assertIn("docs/superpowers/plans", rules_text)
self.assertNotIn("plan_progress.py", rules_text)
self.assertIn("记录 `phase=planning` 与 `spec=<path>`", rules_text)
self.assertIn(
"记录 `plan=<path>`、`executor=executing-plans`、",
rules_text,
)
self.assertIn("未领取 Plan 前,不得直接进入 `$executing-plans`", rules_text)
self.assertIn("默认执行使用 `$executing-plans`", rules_text)
self.assertIn("不是默认执行器", rules_text)
self.assertNotIn("{{PLAYBOOK_SCRIPTS}}", rules_text)
self.assertFalse(rules_text.endswith("\n\n"))
def test_sync_standards_rewrites_typescript_docs_prefix_for_vendored_playbook(self):
with tempfile.TemporaryDirectory() as tmp_dir:
@@ -61,6 +137,7 @@ langs = [\"cpp\", \"tsl\"]
f"""
[playbook]
project_root = "{tmp_dir}"
deploy_root = "{DEFAULT_DEPLOY_ROOT}"
[vendor]
langs = ["typescript"]
@@ -76,6 +153,7 @@ langs = ["typescript"]
f"""
[playbook]
project_root = "{tmp_dir}"
deploy_root = "{DEFAULT_DEPLOY_ROOT}"
[sync_standards]
langs = ["typescript"]
@@ -96,6 +174,52 @@ langs = ["typescript"]
self.assertIn("`docs/standards/playbook/docs/typescript/", text)
self.assertNotIn("`docs/typescript/", text)
def test_sync_memory_bank_includes_active_context_and_human_readable_progress(self):
with tempfile.TemporaryDirectory() as tmp_dir:
config_body = f"""
[playbook]
project_root = "{tmp_dir}"
deploy_root = "{DEFAULT_DEPLOY_ROOT}"
[sync_rules]
[sync_memory_bank]
project_name = "MyProject"
"""
config_path = Path(tmp_dir) / "playbook.toml"
config_path.write_text(config_body, encoding="utf-8")
result = run_cli("-config", str(config_path))
self.assertEqual(result.returncode, 0, msg=result.stderr)
active_context = Path(tmp_dir) / "memory-bank" / "active-context.md"
self.assertTrue(active_context.is_file())
progress = Path(tmp_dir) / "memory-bank" / "progress.md"
progress_text = progress.read_text(encoding="utf-8")
self.assertIn("## Current Focus", progress_text)
self.assertIn("## 状态块示例", progress_text)
self.assertIn("phase: planning", progress_text)
self.assertIn("executor: executing-plans", progress_text)
self.assertIn("<!-- workflow-state:start -->", progress_text)
self.assertIn("<!-- workflow-state:end -->", progress_text)
self.assertIn("## Plan Status", progress_text)
self.assertIn("<!-- plan-status:start -->", progress_text)
self.assertIn("<!-- plan-status:end -->", progress_text)
system_patterns = Path(tmp_dir) / "memory-bank" / "system-patterns.md"
system_patterns_text = system_patterns.read_text(encoding="utf-8")
self.assertIn("# 系统模式与约束", system_patterns_text)
self.assertIn("## 核心不变量", system_patterns_text)
agents_md = Path(tmp_dir) / "AGENTS.md"
agents_text = agents_md.read_text(encoding="utf-8")
self.assertIn("memory-bank/active-context.md", agents_text)
rules_md = Path(tmp_dir) / "AGENT_RULES.md"
rules_text = rules_md.read_text(encoding="utf-8")
self.assertIn("memory-bank/active-context.md", rules_text)
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,186 @@
import json
import os
import shutil
import subprocess
import tempfile
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
MANIFEST = ROOT / ".gitea" / "ci" / "thirdparty_skills.json"
WORKFLOW = ROOT / ".gitea" / "workflows" / "update-thirdparty-skills.yml"
LEGACY_WORKFLOW = ROOT / ".gitea" / "workflows" / "update-thirdparty-superpowers.yml"
UPDATE_SCRIPT = ROOT / ".gitea" / "ci" / "update_thirdparty_skills.sh"
SYNC_SCRIPT = ROOT / ".gitea" / "ci" / "sync_thirdparty_skills.sh"
SKILLS_MD = ROOT / "SKILLS.md"
SUPERPOWERS_LIST = ROOT / "skills" / "thirdparty" / ".sources" / "superpowers.list"
UI_UX_PRO_MAX_LIST = ROOT / "skills" / "thirdparty" / ".sources" / "ui-ux-pro-max.list"
UI_UX_PRO_MAX_DIR = ROOT / "skills" / "thirdparty" / "ui-ux-pro-max"
def load_manifest() -> dict:
return json.loads(MANIFEST.read_text(encoding="utf-8"))
def bash_path(path: Path) -> str:
resolved = path.resolve()
if os.name != "nt":
return resolved.as_posix()
drive = resolved.drive.rstrip(":").lower()
rest = resolved.as_posix()[2:]
return f"/mnt/{drive}{rest}"
def run_command(*args: str, cwd: Path | None = None) -> subprocess.CompletedProcess[str]:
return subprocess.run(
list(args),
cwd=cwd,
capture_output=True,
text=True,
)
class ThirdpartySkillsPipelineTests(unittest.TestCase):
def test_manifest_declares_all_thirdparty_sources(self):
data = load_manifest()
self.assertEqual(
[entry["id"] for entry in data["sources"]],
["superpowers", "ui-ux-pro-max", "andrej-karpathy-skills"],
)
def test_karpathy_manifest_uses_copy_skill_dirs_sync_mode(self):
data = load_manifest()
karpathy = next(
item for item in data["sources"] if item["id"] == "andrej-karpathy-skills"
)
self.assertEqual(karpathy["sync_mode"], "copy_skill_dirs")
self.assertEqual(karpathy["snapshot_dir"], "andrej-karpathy-skills")
self.assertEqual(karpathy["skills_subdir"], "skills")
self.assertEqual(
karpathy["source_list"], "skills/thirdparty/.sources/andrej-karpathy-skills.list"
)
def test_ui_ux_pro_max_uses_render_skill_sync_mode(self):
data = load_manifest()
ui_skill = next(item for item in data["sources"] if item["id"] == "ui-ux-pro-max")
self.assertEqual(ui_skill["sync_mode"], "render_skill")
self.assertEqual(ui_skill["snapshot_dir"], "ui-ux-pro-max")
def test_superpowers_manifest_prunes_non_superpowers_paths(self):
data = load_manifest()
superpowers = next(item for item in data["sources"] if item["id"] == "superpowers")
self.assertEqual(superpowers["remove_paths"], ["skills/ui-ux-pro-max"])
def test_workflow_uses_generic_scripts_and_single_serial_job(self):
text = WORKFLOW.read_text(encoding="utf-8")
self.assertFalse(LEGACY_WORKFLOW.exists())
self.assertIn("update_and_sync:", text)
self.assertNotIn("\n update:\n", text)
self.assertNotIn("\n sync:\n", text)
self.assertIn("bash .gitea/ci/update_thirdparty_skills.sh", text)
self.assertIn("bash .gitea/ci/sync_thirdparty_skills.sh", text)
self.assertNotIn("git merge", text)
self.assertNotIn("git pull", text)
def test_workflow_has_serial_concurrency_and_literal_generic_paths(self):
text = WORKFLOW.read_text(encoding="utf-8")
self.assertIn("concurrency:", text)
self.assertIn("update-thirdparty-${{ github.repository }}", text)
self.assertIn('MANIFEST_PATH: ".gitea/ci/thirdparty_skills.json"', text)
self.assertIn('TARGET_BRANCH="$THIRDPARTY_BRANCH" bash .gitea/ci/update_thirdparty_skills.sh', text)
self.assertIn('TARGET_BRANCH="main" \\', text)
self.assertIn('MANIFEST_PATH="$MANIFEST_PATH" \\', text)
def test_generic_scripts_exist_and_use_manifest(self):
update_text = UPDATE_SCRIPT.read_text(encoding="utf-8")
sync_text = SYNC_SCRIPT.read_text(encoding="utf-8")
self.assertIn('MANIFEST_PATH="${MANIFEST_PATH:-.gitea/ci/thirdparty_skills.json}"', update_text)
self.assertIn('MANIFEST_PATH="${MANIFEST_PATH:-.gitea/ci/thirdparty_skills.json}"', sync_text)
self.assertIn('TARGET_BRANCH="${TARGET_BRANCH:-thirdparty/skill}"', update_text)
self.assertIn('TARGET_BRANCH="${TARGET_BRANCH:-main}"', sync_text)
self.assertIn(':package: deps(thirdparty): update snapshots', update_text)
self.assertIn(':package: deps(skills): sync thirdparty skills', sync_text)
def test_skills_doc_points_to_generic_thirdparty_sources(self):
text = SKILLS_MD.read_text(encoding="utf-8")
self.assertIn("## 9. Third-party Skills", text)
self.assertIn("来源:`skills/thirdparty/.sources/`(第三方来源清单目录)。", text)
self.assertNotIn("Third-party Skills (superpowers)", text)
def test_superpowers_and_ui_ux_pro_max_source_lists_exist(self):
self.assertTrue(SUPERPOWERS_LIST.is_file())
self.assertTrue(UI_UX_PRO_MAX_LIST.is_file())
self.assertIn("using-superpowers", SUPERPOWERS_LIST.read_text(encoding="utf-8"))
self.assertIn("ui-ux-pro-max", UI_UX_PRO_MAX_LIST.read_text(encoding="utf-8"))
def test_ui_ux_pro_max_output_exists_with_data_and_scripts(self):
self.assertTrue((UI_UX_PRO_MAX_DIR / "SKILL.md").is_file())
self.assertTrue((UI_UX_PRO_MAX_DIR / "data").is_dir())
self.assertTrue((UI_UX_PRO_MAX_DIR / "scripts").is_dir())
def test_update_script_materializes_manifest_before_target_checkout(self):
text = UPDATE_SCRIPT.read_text(encoding="utf-8")
self.assertIn('manifest_copy="$tmp_dir/thirdparty_skills.json"', text)
self.assertIn('cp "$MANIFEST_PATH" "$manifest_copy"', text)
self.assertIn('MANIFEST_PATH="$manifest_copy"', text)
self.assertIn("remove_paths", text)
self.assertIn('remove_snapshot_paths "$snapshot_dir" "$remove_paths"', text)
self.assertIn("- Remove-Paths:", text)
self.assertIn('if ! emit_sources_tsv > "$sources_file"; then', text)
self.assertNotIn("done < <(emit_sources_tsv)", text)
self.assertLess(
text.index('cp "$MANIFEST_PATH" "$manifest_copy"'),
text.index('git checkout -B "$TARGET_BRANCH" "origin/$TARGET_BRANCH"'),
)
def test_sync_script_assumes_thirdparty_snapshot_is_already_clean(self):
text = SYNC_SCRIPT.read_text(encoding="utf-8")
self.assertIn('"\\x1f".join(', text)
self.assertIn("while IFS=$'\\x1f' read -r", text)
self.assertNotIn("while IFS=$'\\t' read -r", text)
self.assertNotIn("exclude_skill_dirs", text)
self.assertNotIn("is_excluded_skill_dir", text)
def test_sync_script_generates_karpathy_outputs_in_temp_repo(self):
with tempfile.TemporaryDirectory() as tmp_dir:
tmp_root = Path(tmp_dir)
mirror = tmp_root / "origin.git"
work = tmp_root / "work"
clone_mirror = run_command("git", "clone", "--mirror", str(ROOT), str(mirror))
self.assertEqual(clone_mirror.returncode, 0, msg=clone_mirror.stderr)
clone_work = run_command("git", "clone", str(mirror), str(work))
self.assertEqual(clone_work.returncode, 0, msg=clone_work.stderr)
set_remote = run_command(
"git", "-C", str(work), "remote", "set-url", "origin", bash_path(mirror)
)
self.assertEqual(set_remote.returncode, 0, msg=set_remote.stderr)
shutil.copy2(MANIFEST, work / ".gitea" / "ci" / "thirdparty_skills.json")
shutil.copy2(SYNC_SCRIPT, work / ".gitea" / "ci" / "sync_thirdparty_skills.sh")
sync_result = run_command("bash", ".gitea/ci/sync_thirdparty_skills.sh", cwd=work)
self.assertEqual(
sync_result.returncode,
0,
msg=sync_result.stdout + sync_result.stderr,
)
generated_list = (
work / "skills" / "thirdparty" / ".sources" / "andrej-karpathy-skills.list"
)
generated_skill = (
work / "skills" / "thirdparty" / "karpathy-guidelines" / "SKILL.md"
)
self.assertTrue(generated_list.is_file())
self.assertTrue(generated_skill.is_file())
self.assertIn(
"karpathy-guidelines", generated_list.read_text(encoding="utf-8")
)
if __name__ == "__main__":
unittest.main()
@@ -1,4 +1,6 @@
import tempfile
import unittest
from pathlib import Path
from scripts import playbook
@@ -18,6 +20,44 @@ key = 1
with self.assertRaises(ValueError):
playbook.loads_toml_minimal(raw)
def test_load_config_preserves_windows_users_path_in_basic_string(self):
with tempfile.TemporaryDirectory() as tmp_dir:
config_path = Path(tmp_dir) / "playbook.toml"
config_path.write_text(
'[playbook]\nproject_root = "C:\\Users\\demo\\workspace"\n',
encoding="utf-8",
)
data = playbook.load_config(config_path)
self.assertEqual(data["playbook"]["project_root"], r"C:\Users\demo\workspace")
def test_load_config_preserves_windows_escape_like_segments_for_path_keys(self):
with tempfile.TemporaryDirectory() as tmp_dir:
config_path = Path(tmp_dir) / "playbook.toml"
config_path.write_text(
'[playbook]\nproject_root = "C:\\tmp\\notes"\n\n'
'[install_skills]\nagents_home = "D:\\new\\tab"\n',
encoding="utf-8",
)
data = playbook.load_config(config_path)
self.assertEqual(data["playbook"]["project_root"], r"C:\tmp\notes")
self.assertEqual(data["install_skills"]["agents_home"], r"D:\new\tab")
def test_load_config_keeps_already_escaped_windows_path(self):
with tempfile.TemporaryDirectory() as tmp_dir:
config_path = Path(tmp_dir) / "playbook.toml"
config_path.write_text(
'[playbook]\nproject_root = "C:\\\\Users\\\\demo\\\\workspace"\n',
encoding="utf-8",
)
data = playbook.load_config(config_path)
self.assertEqual(data["playbook"]["project_root"], r"C:\Users\demo\workspace")
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,69 @@
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
RULESET_TSL = ROOT / "rulesets" / "tsl" / "index.md"
README = ROOT / "README.md"
PLAYBOOK_EXAMPLE = ROOT / "playbook.toml.example"
SKILLS_DOC = ROOT / "SKILLS.md"
TEMPLATES_CI_README = ROOT / "templates" / "ci" / "README.md"
TEMPLATES_README = ROOT / "templates" / "README.md"
REMOVED_TSL_GUIDE = ROOT / "skills" / "tsl-guide"
class TslEntrypointsConsistencyTests(unittest.TestCase):
def test_ruleset_lists_canonical_tsl_layers(self):
text = RULESET_TSL.read_text(encoding="utf-8")
self.assertIn("docs/tsl/index.md", text)
self.assertIn("docs/tsl/syntax/index.md", text)
self.assertIn("docs/tsl/finance/index.md", text)
self.assertIn("docs/tsl/modules/index.md", text)
self.assertIn("docs/tsl/reference/index.md", text)
def test_readme_only_lists_two_official_deployment_routes(self):
text = README.read_text(encoding="utf-8")
self.assertIn("方式一:git subtree", text)
self.assertIn("方式二:外部 clone 后执行部署", text)
self.assertIn("`project_root`:目标项目根目录", text)
self.assertIn("`deploy_root`:相对于 `project_root` 的项目内目标目录", text)
self.assertIn("不是外部 clone 出来的 Playbook 仓库路径", text)
self.assertIn("外部 clone 场景下必须显式填写 `deploy_root`", text)
self.assertNotIn("方式二:手动复制快照", text)
self.assertNotIn("方式三:CLI 裁剪复制", text)
self.assertNotIn("如果省略 `deploy_root`,默认仍部署到 `docs/standards/playbook`", text)
def test_playbook_example_defines_deploy_root_as_target_path(self):
text = PLAYBOOK_EXAMPLE.read_text(encoding="utf-8")
self.assertIn('deploy_root = "docs/standards/playbook"', text)
self.assertIn("相对于 project_root", text)
self.assertIn("不是外部 clone 的 playbook 路径", text)
self.assertIn("从外部 clone 执行时必填", text)
self.assertNotIn("target_dir", text)
def test_deployment_docs_do_not_reference_legacy_terms(self):
self.assertNotIn("vendoring", README.read_text(encoding="utf-8"))
self.assertNotIn("`.tmp`", README.read_text(encoding="utf-8"))
self.assertNotIn("vendoring", SKILLS_DOC.read_text(encoding="utf-8"))
self.assertNotIn("vendoring", TEMPLATES_CI_README.read_text(encoding="utf-8"))
def test_repo_docs_use_platform_neutral_skills_source_dir(self):
readme_text = README.read_text(encoding="utf-8")
skills_text = SKILLS_DOC.read_text(encoding="utf-8")
templates_text = TEMPLATES_README.read_text(encoding="utf-8")
self.assertIn("`skills/`", readme_text)
self.assertIn("`skills/`", skills_text)
self.assertIn("├── skills/", templates_text)
self.assertNotIn("`codex/skills/`", readme_text)
self.assertNotIn("`codex/skills/`", skills_text)
self.assertNotIn("├── codex/skills/", templates_text)
def test_repo_no_longer_ships_tsl_guide_skill(self):
self.assertFalse(REMOVED_TSL_GUIDE.exists())
self.assertNotIn("tsl-guide", README.read_text(encoding="utf-8"))
self.assertNotIn("tsl-guide", SKILLS_DOC.read_text(encoding="utf-8"))
self.assertNotIn("$tsl-guide", RULESET_TSL.read_text(encoding="utf-8"))
if __name__ == "__main__":
unittest.main()
@@ -6,6 +6,7 @@ from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
SCRIPT = ROOT / "scripts" / "playbook.py"
DEFAULT_DEPLOY_ROOT = "docs/standards/playbook"
def run_cli(*args):
@@ -22,6 +23,7 @@ class VendorSnapshotTemplatesTests(unittest.TestCase):
config_body = f"""
[playbook]
project_root = "{tmp_dir}"
deploy_root = "{DEFAULT_DEPLOY_ROOT}"
[vendor]
langs = ["tsl"]
@@ -38,6 +40,8 @@ langs = ["tsl"]
self.assertTrue((snapshot / "templates/README.md").is_file())
self.assertTrue((snapshot / "templates/memory-bank").is_dir())
self.assertTrue((snapshot / "templates/prompts").is_dir())
self.assertTrue((snapshot / "skills").is_dir())
self.assertFalse((snapshot / "codex").exists())
if __name__ == "__main__":