From 54047b2b074ef638d0123f2755da0b5f26ec8133 Mon Sep 17 00:00:00 2001 From: Sunny <277479420@qq.com> Date: Sat, 14 Mar 2026 01:01:36 +0800 Subject: [PATCH 01/70] =?UTF-8?q?refactor(tests):=20=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=E5=AE=A1=E6=9F=A5=E4=BF=AE=E5=A4=8D=E2=80=94=E2=80=94=E6=B6=88?= =?UTF-8?q?=E9=99=A4=E9=87=8D=E5=A4=8D=E3=80=81=E4=BF=AE=E6=AD=A3=E7=BC=96?= =?UTF-8?q?=E5=8F=B7=E5=86=B2=E7=AA=81=E3=80=81=E5=A2=9E=E5=BC=BA=E5=81=A5?= =?UTF-8?q?=E5=A3=AE=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - TC 编号冲突:test_sync_maintenance TC-SM→TC-SYN,test_scripts TC-SC→TC-SCR - DRY:_parse_frontmatter/_headings/_product_md_files 提取到 conftest.py 共享 - parse_frontmatter 使用 find() 替代 index() 防止畸形 frontmatter 崩溃 - CR_STATES 8 态确认(含可选 released),修正 docstring 和硬编码状态列表 - _run_script dead code pass→实际 assert 检查退出码 - EXPECTED_TEMPLATES 去重,引用 conftest.TEMPLATE_FILES - test_hooks.py import yaml 移到文件顶部 - SKILL_NAMES 按字母序重排 - TC-CR-05 移到正确位置(04 和 06 之间) - TC-HK-07 添加最小覆盖断言(≥3 个状态) 392 passed, 23 skipped — 全部通过,与修复前一致。 Co-Authored-By: Claude Opus 4.6 --- tests/conftest.py | 47 ++++++++--- tests/static/test_agent_skill_tools.py | 19 ++--- tests/static/test_cross_references.py | 79 ++++++++---------- tests/static/test_frontmatter.py | 24 ++---- tests/static/test_hooks.py | 6 +- tests/static/test_layer_separation.py | 14 +--- tests/static/test_markdown_structure.py | 13 +-- tests/static/test_pace_init.py | 40 +++------- tests/static/test_schema_compliance.py | 14 +--- tests/static/test_scripts.py | 101 ++++++++++++------------ tests/static/test_state_machine.py | 4 +- tests/static/test_sync_maintenance.py | 18 ++--- 12 files changed, 169 insertions(+), 210 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index e38a4e8..dc66df2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,8 +1,10 @@ """Shared fixtures and constants for devpace test suite.""" +import re from pathlib import Path import pytest +import yaml # ── Paths ────────────────────────────────────────────────────────────────── DEVPACE_ROOT = Path(__file__).resolve().parent.parent # devpace/ @@ -25,9 +27,9 @@ "pace-release", "pace-retro", "pace-review", - "pace-sync", "pace-role", "pace-status", + "pace-sync", "pace-test", "pace-theory", "pace-trace", @@ -129,6 +131,25 @@ ("deployed", "rolled_back"), ] +# ── Shared utilities ───────────────────────────────────────────────────── + +def parse_frontmatter(path): + """Extract YAML frontmatter; returns None if missing or malformed.""" + text = path.read_text(encoding="utf-8") + if not text.startswith("---"): + return None + end = text.find("---", 3) + if end == -1: + return None + return yaml.safe_load(text[3:end]) + + +def headings(text): + """Extract markdown headings as (level, title) tuples.""" + return [(len(m.group(1)), m.group(2).strip()) + for m in re.finditer(r'^(#{1,6})\s+(.+)$', text, re.MULTILINE)] + + # ── Workspace exclusion ────────────────────────────────────────────────── # skill-creator evaluation workspaces (*-workspace/) are gitignored but may # exist on disk. Tests scanning product-layer directories must skip them. @@ -137,24 +158,26 @@ def _is_workspace_path(p: Path) -> bool: """Return True if any ancestor directory name ends with '-workspace'.""" return any(part.endswith("-workspace") for part in p.parts) -# ── Fixtures ─────────────────────────────────────────────────────────────── - -@pytest.fixture -def devpace_root(): - """Return the devpace project root as a Path.""" - return DEVPACE_ROOT - -@pytest.fixture -def product_md_files(): - """Yield all .md files under product-layer directories.""" +def product_md_files(exclude_workspace=True): + """Collect all .md files under product-layer directories.""" files = [] for d in PRODUCT_DIRS: dirpath = DEVPACE_ROOT / d if dirpath.is_dir(): - files.extend(dirpath.rglob("*.md")) + for f in dirpath.rglob("*.md"): + if exclude_workspace and _is_workspace_path(f): + continue + files.append(f) return files +# ── Fixtures ─────────────────────────────────────────────────────────────── + +@pytest.fixture +def devpace_root(): + """Return the devpace project root as a Path.""" + return DEVPACE_ROOT + @pytest.fixture def skill_dirs(): diff --git a/tests/static/test_agent_skill_tools.py b/tests/static/test_agent_skill_tools.py index 9ee5fc7..d0ba1eb 100644 --- a/tests/static/test_agent_skill_tools.py +++ b/tests/static/test_agent_skill_tools.py @@ -1,16 +1,7 @@ """TC-AST: Agent-Skill tool consistency for context:fork skills.""" import pytest import yaml -from tests.conftest import DEVPACE_ROOT, SKILL_NAMES - - -def _parse_frontmatter(path): - """Extract YAML frontmatter from a markdown file.""" - text = path.read_text(encoding="utf-8") - if not text.startswith("---"): - return None - end = text.index("---", 3) - return yaml.safe_load(text[3:end]) +from tests.conftest import DEVPACE_ROOT, SKILL_NAMES, parse_frontmatter def _parse_tools_list(raw): @@ -34,7 +25,7 @@ def _forked_skill_agent_pairs(): skill_md = skills_root / name / "SKILL.md" if not skill_md.exists(): continue - fm = _parse_frontmatter(skill_md) + fm = parse_frontmatter(skill_md) if fm is None: continue if fm.get("context") != "fork" or "agent" not in fm: @@ -85,7 +76,7 @@ def test_tc_ast_02_agent_has_tools( """TC-AST-02: The referenced Agent must declare a tools field.""" if not agent_path.exists(): pytest.skip(f"Agent file {agent_path.name} missing (covered by TC-AST-01)") - fm = _parse_frontmatter(agent_path) + fm = parse_frontmatter(agent_path) assert fm is not None, f"Agent {agent_name} has no frontmatter" assert "tools" in fm, ( f"Agent '{agent_name}' (used by Skill '{skill_name}') " @@ -109,8 +100,8 @@ def test_tc_ast_03_agent_tools_superset_of_skill( if not agent_path.exists(): pytest.skip(f"Agent file {agent_path.name} missing (covered by TC-AST-01)") - skill_fm = _parse_frontmatter(skill_path) - agent_fm = _parse_frontmatter(agent_path) + skill_fm = parse_frontmatter(skill_path) + agent_fm = parse_frontmatter(agent_path) if skill_fm is None or "allowed-tools" not in skill_fm: pytest.skip(f"Skill '{skill_name}' has no allowed-tools") diff --git a/tests/static/test_cross_references.py b/tests/static/test_cross_references.py index 502c896..c047a10 100644 --- a/tests/static/test_cross_references.py +++ b/tests/static/test_cross_references.py @@ -1,7 +1,7 @@ """TC-CR: Cross-reference integrity between product-layer files.""" import re import pytest -from tests.conftest import DEVPACE_ROOT, PRODUCT_DIRS, SKILL_NAMES +from tests.conftest import DEVPACE_ROOT, PRODUCT_DIRS, SKILL_NAMES, product_md_files LINK_RE = re.compile(r'\[([^\]]*)\]\(([^)]+)\)') FENCE_RE = re.compile(r'```[^\n]*\n.*?```', re.DOTALL) @@ -15,21 +15,12 @@ def _strip_code(content: str) -> str: return content -def _product_md_files(): - files = [] - for d in PRODUCT_DIRS: - dirpath = DEVPACE_ROOT / d - if dirpath.is_dir(): - files.extend(dirpath.rglob("*.md")) - return files - - @pytest.mark.static class TestCrossReferences: def test_tc_cr_01_internal_links_valid(self): """TC-CR-01: Markdown internal links point to existing files.""" broken = [] - for f in _product_md_files(): + for f in product_md_files(): content = f.read_text(encoding="utf-8") # Strip code blocks — example links should not be validated content = _strip_code(content) @@ -85,6 +76,38 @@ def test_tc_cr_04_init_template_refs_exist(self): templates = list(template_dir.glob("*.md")) assert len(templates) >= 7, f"Expected ≥7 templates, found {len(templates)}" + def test_tc_cr_05_claude_md_template_synced_with_rules(self): + """TC-CR-05: claude-md-devpace.md template contains key content or delegates to rules.""" + template = DEVPACE_ROOT / "skills" / "pace-init" / "templates" / "claude-md-devpace.md" + rules = DEVPACE_ROOT / "rules" / "devpace-rules.md" + if not template.exists() or not rules.exists(): + pytest.skip("Template or rules file not found") + template_content = template.read_text(encoding="utf-8") + missing = [] + # Template must either contain key concepts directly OR delegate to rules + delegates_to_rules = "devpace-rules.md" in template_content + if not delegates_to_rules: + # §2 dual mode: explore vs advance + if "探索" not in template_content or "推进" not in template_content: + missing.append("§2 双模式(探索/推进)关键词缺失") + # §9 change management trigger words + change_triggers = ["不做了", "加一个", "改一下"] + if not any(t in template_content for t in change_triggers): + missing.append("§9 变更管理触发词缺失(至少需包含一个:不做了/加一个/改一下)") + # Session end summary + if "3-5" not in template_content and "3-5" not in template_content.replace("–", "-"): + missing.append("会话结束 3-5 行摘要规则缺失") + # state.md reference always required (either inline or in file table) + if "state.md" not in template_content: + missing.append("会话开始读 state.md 规则缺失") + # .devpace/ reference always required + if ".devpace/" not in template_content: + missing.append(".devpace/ 文件参考缺失") + assert not missing, ( + f"claude-md-devpace.md template is out of sync with rules:\n" + + "\n".join(f" - {m}" for m in missing) + ) + def test_tc_cr_06_rules_section_refs_valid(self): """TC-CR-06: §N cross-references within rules file point to existing sections.""" rules = DEVPACE_ROOT / "rules" / "devpace-rules.md" @@ -112,7 +135,7 @@ def test_tc_cr_07_detail_refs_exist(self): """TC-CR-07: '详见' backtick references point to existing files.""" broken = [] ref_pattern = re.compile(r'(?:详见|见)\s+`([a-zA-Z0-9_/.-]+\.md)`') - for f in _product_md_files(): + for f in product_md_files(): content = f.read_text(encoding="utf-8") for m in ref_pattern.finditer(content): ref_path = m.group(1) @@ -170,35 +193,3 @@ def test_tc_cr_11_br_schema_refs_valid(self): assert "EPIC" in content or "Epic" in content, "br-format.md missing Epic reference" assert "PF-" in content or "PF" in content, "br-format.md missing PF reference" assert "project.md" in content, "br-format.md missing project.md reference" - - def test_tc_cr_05_claude_md_template_synced_with_rules(self): - """TC-CR-05: claude-md-devpace.md template contains key content or delegates to rules.""" - template = DEVPACE_ROOT / "skills" / "pace-init" / "templates" / "claude-md-devpace.md" - rules = DEVPACE_ROOT / "rules" / "devpace-rules.md" - if not template.exists() or not rules.exists(): - pytest.skip("Template or rules file not found") - template_content = template.read_text(encoding="utf-8") - missing = [] - # Template must either contain key concepts directly OR delegate to rules - delegates_to_rules = "devpace-rules.md" in template_content - if not delegates_to_rules: - # §2 dual mode: explore vs advance - if "探索" not in template_content or "推进" not in template_content: - missing.append("§2 双模式(探索/推进)关键词缺失") - # §9 change management trigger words - change_triggers = ["不做了", "加一个", "改一下"] - if not any(t in template_content for t in change_triggers): - missing.append("§9 变更管理触发词缺失(至少需包含一个:不做了/加一个/改一下)") - # Session end summary - if "3-5" not in template_content and "3-5" not in template_content.replace("–", "-"): - missing.append("会话结束 3-5 行摘要规则缺失") - # state.md reference always required (either inline or in file table) - if "state.md" not in template_content: - missing.append("会话开始读 state.md 规则缺失") - # .devpace/ reference always required - if ".devpace/" not in template_content: - missing.append(".devpace/ 文件参考缺失") - assert not missing, ( - f"claude-md-devpace.md template is out of sync with rules:\n" - + "\n".join(f" - {m}" for m in missing) - ) diff --git a/tests/static/test_frontmatter.py b/tests/static/test_frontmatter.py index 49ae2bf..1936e47 100644 --- a/tests/static/test_frontmatter.py +++ b/tests/static/test_frontmatter.py @@ -1,15 +1,7 @@ """TC-FM: SKILL.md frontmatter validation.""" import pytest import yaml -from tests.conftest import DEVPACE_ROOT, SKILL_NAMES, LEGAL_SKILL_FIELDS, LEGAL_MODEL_VALUES, LEGAL_TOOL_NAMES - -def _parse_frontmatter(path): - """Extract YAML frontmatter from a markdown file.""" - text = path.read_text(encoding="utf-8") - if not text.startswith("---"): - return None - end = text.index("---", 3) - return yaml.safe_load(text[3:end]) +from tests.conftest import DEVPACE_ROOT, SKILL_NAMES, LEGAL_SKILL_FIELDS, LEGAL_MODEL_VALUES, LEGAL_TOOL_NAMES, parse_frontmatter def _skill_md_files(): skills_root = DEVPACE_ROOT / "skills" @@ -31,7 +23,7 @@ def test_tc_fm_01_has_frontmatter(self, name, path): @pytest.mark.parametrize("name,path", _skill_md_files(), ids=[n for n, _ in _skill_md_files()]) def test_tc_fm_02_legal_fields_only(self, name, path): """TC-FM-02: Frontmatter uses only legal fields.""" - fm = _parse_frontmatter(path) + fm = parse_frontmatter(path) if fm is None: pytest.skip(f"{name} has no frontmatter") illegal = set(fm.keys()) - LEGAL_SKILL_FIELDS @@ -40,13 +32,13 @@ def test_tc_fm_02_legal_fields_only(self, name, path): @pytest.mark.parametrize("name,path", _skill_md_files(), ids=[n for n, _ in _skill_md_files()]) def test_tc_fm_03_description_required(self, name, path): """TC-FM-03: description field must exist.""" - fm = _parse_frontmatter(path) + fm = parse_frontmatter(path) assert fm and "description" in fm, f"{name} SKILL.md missing 'description' in frontmatter" @pytest.mark.parametrize("name,path", _skill_md_files(), ids=[n for n, _ in _skill_md_files()]) def test_tc_fm_04_allowed_tools_valid(self, name, path): """TC-FM-04: allowed-tools values are recognized tool names.""" - fm = _parse_frontmatter(path) + fm = parse_frontmatter(path) if fm is None or "allowed-tools" not in fm: pytest.skip(f"{name} has no allowed-tools") tools = [t.strip() for t in fm["allowed-tools"].split(",")] @@ -56,7 +48,7 @@ def test_tc_fm_04_allowed_tools_valid(self, name, path): @pytest.mark.parametrize("name,path", _skill_md_files(), ids=[n for n, _ in _skill_md_files()]) def test_tc_fm_05_model_valid(self, name, path): """TC-FM-05: model field (if present) is sonnet/opus/haiku.""" - fm = _parse_frontmatter(path) + fm = parse_frontmatter(path) if fm is None or "model" not in fm: pytest.skip(f"{name} has no model field") assert fm["model"] in LEGAL_MODEL_VALUES, f"{name} has invalid model: {fm['model']}" @@ -86,7 +78,7 @@ def test_tc_fm_07_file_reading_skills_have_allowed_tools(self, name, path): reads_files = any(kw in body for kw in self._FILE_READ_INDICATORS) if not reads_files: pytest.skip(f"{name} does not appear to read files") - fm = _parse_frontmatter(path) + fm = parse_frontmatter(path) assert fm and "allowed-tools" in fm, ( f"{name} reads files but has no allowed-tools declared" ) @@ -105,7 +97,7 @@ def test_tc_fm_08_argument_hint_present(self, name, path): has_arguments = "$ARGUMENTS" in body or "$0" in body or "$1" in body if not has_arguments: pytest.skip(f"{name} does not use $ARGUMENTS") - fm = _parse_frontmatter(path) + fm = parse_frontmatter(path) if not fm or "argument-hint" not in fm: import warnings warnings.warn( @@ -118,7 +110,7 @@ def test_tc_fm_08_argument_hint_present(self, name, path): @pytest.mark.parametrize("name,path", _skill_md_files(), ids=[n for n, _ in _skill_md_files()]) def test_tc_fm_09_hook_matcher_tools_in_allowed_tools(self, name, path): """TC-FM-09: Hook matcher tool_name entries must be a subset of allowed-tools.""" - fm = _parse_frontmatter(path) + fm = parse_frontmatter(path) if fm is None or "hooks" not in fm or "allowed-tools" not in fm: pytest.skip(f"{name} has no hooks or no allowed-tools") allowed = {t.strip() for t in fm["allowed-tools"].split(",")} diff --git a/tests/static/test_hooks.py b/tests/static/test_hooks.py index 43d6bdc..e909535 100644 --- a/tests/static/test_hooks.py +++ b/tests/static/test_hooks.py @@ -4,6 +4,7 @@ import stat import pytest +import yaml from tests.conftest import DEVPACE_ROOT, CR_STATES HOOKS_DIR = DEVPACE_ROOT / "hooks" @@ -128,6 +129,9 @@ def test_tc_hk_07_pre_tool_use_states_match_conftest(self): f"pre-tool-use hook references state '{state}' " f"not in conftest CR_STATES: {CR_STATES}" ) + assert len(checked_states) >= 3, ( + f"pre-tool-use hook only references {len(checked_states)} CR states, expected ≥3" + ) @pytest.mark.static @@ -178,7 +182,6 @@ def test_tc_hk_11_post_tool_use_failure_configured(self): def test_tc_hk_12_agent_memory_configured(self): """TC-HK-12: All agents have memory:project for cross-session persistence.""" - import yaml agents_dir = DEVPACE_ROOT / "agents" for agent_file in agents_dir.glob("*.md"): content = agent_file.read_text(encoding="utf-8") @@ -193,7 +196,6 @@ def test_tc_hk_12_agent_memory_configured(self): def test_tc_hk_13_skill_level_hooks_configured(self): """TC-HK-13: pace-dev and pace-review have skill-level hooks.""" - import yaml for skill_name in ["pace-dev", "pace-review"]: skill_path = DEVPACE_ROOT / "skills" / skill_name / "SKILL.md" assert skill_path.exists(), f"SKILL.md not found for {skill_name}" diff --git a/tests/static/test_layer_separation.py b/tests/static/test_layer_separation.py index 4b1a78f..832268c 100644 --- a/tests/static/test_layer_separation.py +++ b/tests/static/test_layer_separation.py @@ -1,24 +1,16 @@ """TC-LS: Product-layer files must not reference dev-layer paths.""" import re import pytest -from tests.conftest import DEVPACE_ROOT, PRODUCT_DIRS, _is_workspace_path +from tests.conftest import DEVPACE_ROOT, PRODUCT_DIRS, product_md_files DEV_PATH_RE = re.compile(r'(?:\.\./|\./)?(?:docs/|\.claude/)') -def _product_md_files(): - files = [] - for d in PRODUCT_DIRS: - dirpath = DEVPACE_ROOT / d - if dirpath.is_dir(): - files.extend(f for f in dirpath.rglob("*.md") if not _is_workspace_path(f)) - return files - @pytest.mark.static class TestLayerSeparation: def test_tc_ls_01_no_docs_reference(self): """TC-LS-01: Product layer has no docs/ references.""" violations = [] - for f in _product_md_files(): + for f in product_md_files(): for i, line in enumerate(f.read_text(encoding="utf-8").splitlines(), 1): if re.search(r'(? 10, \ f"{name}/SKILL.md too short ({len(content.splitlines())} lines)" @@ -58,7 +53,7 @@ def test_tc_ms_04_procedure_files_structure(self): content = proc.read_text(encoding="utf-8") # Should have numbered steps or headings has_steps = bool(re.search(r'(?:Step|步骤|###)\s*\d', content, re.IGNORECASE)) - has_headings = len(_headings(content)) >= 2 + has_headings = len(headings(content)) >= 2 assert has_steps or has_headings, \ f"{proc.name} lacks clear step structure" diff --git a/tests/static/test_pace_init.py b/tests/static/test_pace_init.py index 3dcc1b8..709b795 100644 --- a/tests/static/test_pace_init.py +++ b/tests/static/test_pace_init.py @@ -14,7 +14,7 @@ import pytest import yaml -from tests.conftest import DEVPACE_ROOT, LEGAL_TOOL_NAMES +from tests.conftest import DEVPACE_ROOT, LEGAL_TOOL_NAMES, TEMPLATE_FILES, parse_frontmatter SKILL_PATH = DEVPACE_ROOT / "skills" / "pace-init" / "SKILL.md" SKILL_DIR = DEVPACE_ROOT / "skills" / "pace-init" @@ -22,15 +22,6 @@ SCHEMA_DIR = DEVPACE_ROOT / "knowledge" / "_schema" -def _parse_frontmatter(path): - """Extract YAML frontmatter from a markdown file.""" - text = path.read_text(encoding="utf-8") - if not text.startswith("---"): - return None - end = text.index("---", 3) - return yaml.safe_load(text[3:end]) - - def _skill_body(): """Return SKILL.md body text (after frontmatter).""" text = SKILL_PATH.read_text(encoding="utf-8") @@ -45,20 +36,7 @@ def _procedure_files(): # ── Expected inventory ────────────────────────────────────────────────────── -EXPECTED_TEMPLATES = [ - "state.md", - "project.md", - "cr.md", - "workflow.md", - "checks.md", - "context.md", - "iteration.md", - "dashboard.md", - "claude-md-devpace.md", - "insights.md", - "integrations.md", - "release.md", -] +EXPECTED_TEMPLATES = TEMPLATE_FILES EXPECTED_PROCEDURES = [ "init-procedures-core.md", @@ -124,7 +102,7 @@ class TestPaceInitFrontmatter: def test_tc_init_01_edit_in_allowed_tools(self): """TC-INIT-01: Edit tool is present in pace-init allowed-tools.""" - fm = _parse_frontmatter(SKILL_PATH) + fm = parse_frontmatter(SKILL_PATH) assert fm and "allowed-tools" in fm tools = [t.strip() for t in fm["allowed-tools"].split(",")] assert "Edit" in tools, ( @@ -133,7 +111,7 @@ def test_tc_init_01_edit_in_allowed_tools(self): def test_tc_init_02_hook_matcher_subset_of_allowed_tools(self): """TC-INIT-02: Hook matcher tool_name entries are a subset of allowed-tools.""" - fm = _parse_frontmatter(SKILL_PATH) + fm = parse_frontmatter(SKILL_PATH) assert fm and "hooks" in fm and "allowed-tools" in fm allowed = {t.strip() for t in fm["allowed-tools"].split(",")} matcher_tools = set() @@ -159,7 +137,7 @@ def test_tc_init_02_hook_matcher_subset_of_allowed_tools(self): def test_tc_init_03_hook_guard_covers_write_targets(self): """TC-INIT-03: Hook guard is a command Hook with scope check script.""" - fm = _parse_frontmatter(SKILL_PATH) + fm = parse_frontmatter(SKILL_PATH) assert fm and "hooks" in fm hooks_found = [] for _event, entries in fm["hooks"].items(): @@ -663,7 +641,7 @@ class TestSubcommandCompleteness: def test_tc_init_60_argument_hint_covers_subcommands(self): """TC-INIT-60: argument-hint in frontmatter lists all documented subcommands.""" - fm = _parse_frontmatter(SKILL_PATH) + fm = parse_frontmatter(SKILL_PATH) assert fm and "argument-hint" in fm hint = fm["argument-hint"] for subcmd in DOCUMENTED_SUBCOMMANDS: @@ -752,7 +730,7 @@ class TestContentQuality: def test_tc_init_70_description_starts_with_trigger(self): """TC-INIT-70: Description follows CSO rules — starts with trigger conditions.""" - fm = _parse_frontmatter(SKILL_PATH) + fm = parse_frontmatter(SKILL_PATH) desc = fm["description"] assert desc.startswith("Use when"), ( f"Description should start with 'Use when' per CSO rules. Got: {desc[:60]}..." @@ -760,7 +738,7 @@ def test_tc_init_70_description_starts_with_trigger(self): def test_tc_init_71_description_has_not_for_exclusions(self): """TC-INIT-71: Description includes NOT-for exclusions to prevent mis-triggering.""" - fm = _parse_frontmatter(SKILL_PATH) + fm = parse_frontmatter(SKILL_PATH) desc = fm["description"] assert "NOT" in desc, ( "Description missing NOT-for exclusions for disambiguation" @@ -768,7 +746,7 @@ def test_tc_init_71_description_has_not_for_exclusions(self): def test_tc_init_72_description_has_trigger_keywords(self): """TC-INIT-72: Description includes Chinese trigger keywords.""" - fm = _parse_frontmatter(SKILL_PATH) + fm = parse_frontmatter(SKILL_PATH) desc = fm["description"] keywords = ["初始化", "pace-init"] for kw in keywords: diff --git a/tests/static/test_schema_compliance.py b/tests/static/test_schema_compliance.py index 0ac50a1..dee108a 100644 --- a/tests/static/test_schema_compliance.py +++ b/tests/static/test_schema_compliance.py @@ -1,22 +1,16 @@ """TC-SC: Template files comply with schema contracts.""" import re import pytest -from tests.conftest import DEVPACE_ROOT +from tests.conftest import DEVPACE_ROOT, headings TEMPLATE_DIR = DEVPACE_ROOT / "skills" / "pace-init" / "templates" SCHEMA_DIR = DEVPACE_ROOT / "knowledge" / "_schema" METRICS_FILE = DEVPACE_ROOT / "knowledge" / "metrics.md" -def _headings(text): - """Extract markdown headings as (level, title) tuples.""" - return [(len(m.group(1)), m.group(2).strip()) - for m in re.finditer(r'^(#{1,6})\s+(.+)$', text, re.MULTILINE)] - - def _has_heading(text, title_substr): """Check if text contains a heading with given substring.""" - return any(title_substr in h[1] for h in _headings(text)) + return any(title_substr in h[1] for h in headings(text)) def _table_columns(text, table_heading_substr=None): @@ -71,9 +65,9 @@ def test_tc_sc_03_state_template(self): assert "devpace-version" in content, "state.md should have version marker" def test_tc_sc_04_workflow_template(self): - """TC-SC-04: workflow.md defines all 7 states and transitions.""" + """TC-SC-04: workflow.md defines all 8 CR states and transitions.""" content = (TEMPLATE_DIR / "workflow.md").read_text(encoding="utf-8") - for state in ["created", "developing", "verifying", "in_review", "approved", "merged", "paused"]: + for state in ["created", "developing", "verifying", "in_review", "approved", "merged", "released", "paused"]: assert state in content, f"workflow.md missing state: {state}" assert "→" in content or "->" in content, "workflow.md missing transition arrows" diff --git a/tests/static/test_scripts.py b/tests/static/test_scripts.py index 2cce7e5..664e85f 100644 --- a/tests/static/test_scripts.py +++ b/tests/static/test_scripts.py @@ -1,4 +1,4 @@ -"""TC-SC: Script output validation — verifies JSON structure of all .mjs scripts.""" +"""TC-SCR: Script output validation — verifies JSON structure of all .mjs scripts.""" import json import os import subprocess @@ -106,9 +106,10 @@ def _run_script(script_name, args=None, stdin_data=None, expect_exit_0=True): input=stdin_data, ) if expect_exit_0 and result.returncode != 0: - # Some scripts (security-scan, validate-schema) use non-zero for findings/errors - # Only fail if we explicitly expected exit 0 - pass + assert result.returncode == 0, ( + f"Script {script_name} exited with {result.returncode}.\n" + f"stderr: {result.stderr}" + ) stdout = result.stdout.strip() assert stdout, ( f"Script {script_name} produced no stdout.\n" @@ -121,16 +122,16 @@ def _run_script(script_name, args=None, stdin_data=None, expect_exit_0=True): class TestExtractCrMetadata: """Tests for extract-cr-metadata.mjs JSON output.""" - def test_tc_sc_01_returns_json_array(self, tmp_path): - """TC-SC-01: extract-cr-metadata returns a JSON array.""" + def test_tc_scr_01_returns_json_array(self, tmp_path): + """TC-SCR-01: extract-cr-metadata returns a JSON array.""" devpace_dir = _make_devpace(tmp_path) data, rc = _run_script("extract-cr-metadata.mjs", [devpace_dir]) assert rc == 0 assert isinstance(data, list) assert len(data) == 2 - def test_tc_sc_02_cr_has_required_fields(self, tmp_path): - """TC-SC-02: Each CR object has required metadata fields.""" + def test_tc_scr_02_cr_has_required_fields(self, tmp_path): + """TC-SCR-02: Each CR object has required metadata fields.""" devpace_dir = _make_devpace(tmp_path) data, _ = _run_script("extract-cr-metadata.mjs", [devpace_dir]) required_fields = {"id", "title", "type", "status", "events", "breaking", "fileName"} @@ -138,16 +139,16 @@ def test_tc_sc_02_cr_has_required_fields(self, tmp_path): missing = required_fields - set(cr.keys()) assert not missing, f"CR {cr.get('id', '?')} missing fields: {missing}" - def test_tc_sc_03_status_filter_works(self, tmp_path): - """TC-SC-03: --status filter returns only matching CRs.""" + def test_tc_scr_03_status_filter_works(self, tmp_path): + """TC-SCR-03: --status filter returns only matching CRs.""" devpace_dir = _make_devpace(tmp_path) data, _ = _run_script("extract-cr-metadata.mjs", [devpace_dir, "--status", "merged"]) assert all(cr["status"] == "merged" for cr in data) assert len(data) == 1 assert data[0]["id"] == "CR-002" - def test_tc_sc_04_events_parsed(self, tmp_path): - """TC-SC-04: Events table is parsed into array.""" + def test_tc_scr_04_events_parsed(self, tmp_path): + """TC-SCR-04: Events table is parsed into array.""" devpace_dir = _make_devpace(tmp_path) data, _ = _run_script("extract-cr-metadata.mjs", [devpace_dir, "--id", "CR-001"]) assert len(data) == 1 @@ -161,8 +162,8 @@ def test_tc_sc_04_events_parsed(self, tmp_path): class TestValidateSchema: """Tests for validate-schema.mjs JSON output.""" - def test_tc_sc_05_returns_json_with_results(self, tmp_path): - """TC-SC-05: validate-schema returns JSON with results array.""" + def test_tc_scr_05_returns_json_with_results(self, tmp_path): + """TC-SCR-05: validate-schema returns JSON with results array.""" devpace_dir = _make_devpace(tmp_path) data, _ = _run_script("validate-schema.mjs", [devpace_dir], expect_exit_0=False) assert "results" in data @@ -171,15 +172,15 @@ def test_tc_sc_05_returns_json_with_results(self, tmp_path): assert "errors" in data assert "warnings" in data - def test_tc_sc_06_cr_validation_has_correct_type(self, tmp_path): - """TC-SC-06: CR file validation identifies type as 'cr'.""" + def test_tc_scr_06_cr_validation_has_correct_type(self, tmp_path): + """TC-SCR-06: CR file validation identifies type as 'cr'.""" devpace_dir = _make_devpace(tmp_path) data, _ = _run_script("validate-schema.mjs", [devpace_dir, "--type", "cr"], expect_exit_0=False) for result in data["results"]: assert result["type"] == "cr" - def test_tc_sc_07_each_result_has_structure(self, tmp_path): - """TC-SC-07: Each validation result has file/type/valid/errors/warnings.""" + def test_tc_scr_07_each_result_has_structure(self, tmp_path): + """TC-SCR-07: Each validation result has file/type/valid/errors/warnings.""" devpace_dir = _make_devpace(tmp_path) data, _ = _run_script("validate-schema.mjs", [devpace_dir], expect_exit_0=False) required_fields = {"file", "type", "valid", "errors", "warnings"} @@ -187,8 +188,8 @@ def test_tc_sc_07_each_result_has_structure(self, tmp_path): missing = required_fields - set(result.keys()) assert not missing, f"Result for {result.get('file', '?')} missing fields: {missing}" - def test_tc_sc_08_valid_cr_passes(self, tmp_path): - """TC-SC-08: A well-formed CR passes validation.""" + def test_tc_scr_08_valid_cr_passes(self, tmp_path): + """TC-SCR-08: A well-formed CR passes validation.""" devpace_dir = _make_devpace(tmp_path) data, _ = _run_script( "validate-schema.mjs", @@ -203,8 +204,8 @@ def test_tc_sc_08_valid_cr_passes(self, tmp_path): class TestCollectSignals: """Tests for collect-signals.mjs JSON output.""" - def test_tc_sc_09_returns_json_with_triggered(self, tmp_path): - """TC-SC-09: collect-signals returns JSON with triggered array.""" + def test_tc_scr_09_returns_json_with_triggered(self, tmp_path): + """TC-SCR-09: collect-signals returns JSON with triggered array.""" devpace_dir = _make_devpace(tmp_path) data, rc = _run_script("collect-signals.mjs", [devpace_dir]) assert rc == 0 @@ -214,8 +215,8 @@ def test_tc_sc_09_returns_json_with_triggered(self, tmp_path): assert "role" in data assert "timestamp" in data - def test_tc_sc_10_signals_have_required_fields(self, tmp_path): - """TC-SC-10: Each triggered signal has id/group/label/detail.""" + def test_tc_scr_10_signals_have_required_fields(self, tmp_path): + """TC-SCR-10: Each triggered signal has id/group/label/detail.""" devpace_dir = _make_devpace(tmp_path) data, _ = _run_script("collect-signals.mjs", [devpace_dir]) required_fields = {"id", "group", "label", "detail"} @@ -223,22 +224,22 @@ def test_tc_sc_10_signals_have_required_fields(self, tmp_path): missing = required_fields - set(signal.keys()) assert not missing, f"Signal {signal.get('id', '?')} missing fields: {missing}" - def test_tc_sc_11_developing_cr_triggers_s3(self, tmp_path): - """TC-SC-11: A developing CR triggers S3 (continue development).""" + def test_tc_scr_11_developing_cr_triggers_s3(self, tmp_path): + """TC-SCR-11: A developing CR triggers S3 (continue development).""" devpace_dir = _make_devpace(tmp_path) data, _ = _run_script("collect-signals.mjs", [devpace_dir]) signal_ids = [s["id"] for s in data["triggered"]] assert "S3" in signal_ids, f"Expected S3 for developing CR, got: {signal_ids}" - def test_tc_sc_12_role_reorder_respected(self, tmp_path): - """TC-SC-12: --role flag is reflected in output.""" + def test_tc_scr_12_role_reorder_respected(self, tmp_path): + """TC-SCR-12: --role flag is reflected in output.""" devpace_dir = _make_devpace(tmp_path) data, _ = _run_script("collect-signals.mjs", [devpace_dir, "--role", "pm"]) assert data["role"] == "pm" - def test_tc_sc_21_s4_paused_with_resolved_blocker(self, tmp_path): - """TC-SC-21: S4 triggers when paused CR's blocker is resolved (merged).""" + def test_tc_scr_21_s4_paused_with_resolved_blocker(self, tmp_path): + """TC-SCR-21: S4 triggers when paused CR's blocker is resolved (merged).""" d = tmp_path / ".devpace" / "backlog" d.mkdir(parents=True) # CR-001: paused, blocked by CR-002 @@ -287,8 +288,8 @@ def test_tc_sc_21_s4_paused_with_resolved_blocker(self, tmp_path): signal_ids = [s["id"] for s in data["triggered"]] assert "S4" in signal_ids, f"Expected S4 when blocker is resolved, got: {signal_ids}" - def test_tc_sc_22_s4_paused_with_unresolved_blocker(self, tmp_path): - """TC-SC-22: S4 does NOT trigger when paused CR's blocker is still active.""" + def test_tc_scr_22_s4_paused_with_unresolved_blocker(self, tmp_path): + """TC-SCR-22: S4 does NOT trigger when paused CR's blocker is still active.""" d = tmp_path / ".devpace" / "backlog" d.mkdir(parents=True) cr_paused = textwrap.dedent("""\ @@ -333,8 +334,8 @@ def test_tc_sc_22_s4_paused_with_unresolved_blocker(self, tmp_path): signal_ids = [s["id"] for s in data["triggered"]] assert "S4" not in signal_ids, f"S4 should NOT trigger when blocker is still active, got: {signal_ids}" - def test_tc_sc_23_extract_cr_has_blocked_field(self, tmp_path): - """TC-SC-23: extract-cr-metadata includes blocked field.""" + def test_tc_scr_23_extract_cr_has_blocked_field(self, tmp_path): + """TC-SCR-23: extract-cr-metadata includes blocked field.""" d = tmp_path / ".devpace" / "backlog" d.mkdir(parents=True) cr = textwrap.dedent("""\ @@ -366,16 +367,16 @@ def test_tc_sc_23_extract_cr_has_blocked_field(self, tmp_path): class TestComputeMetrics: """Tests for compute-metrics.mjs JSON output.""" - def test_tc_sc_13_returns_json_with_metrics(self, tmp_path): - """TC-SC-13: compute-metrics returns JSON with metrics object.""" + def test_tc_scr_13_returns_json_with_metrics(self, tmp_path): + """TC-SCR-13: compute-metrics returns JSON with metrics object.""" devpace_dir = _make_devpace(tmp_path) data, rc = _run_script("compute-metrics.mjs", [devpace_dir]) assert rc == 0 assert "metrics" in data assert isinstance(data["metrics"], dict) - def test_tc_sc_14_metrics_include_core_indicators(self, tmp_path): - """TC-SC-14: Metrics include expected core indicator keys.""" + def test_tc_scr_14_metrics_include_core_indicators(self, tmp_path): + """TC-SCR-14: Metrics include expected core indicator keys.""" devpace_dir = _make_devpace(tmp_path) data, _ = _run_script("compute-metrics.mjs", [devpace_dir]) metrics = data["metrics"] @@ -387,15 +388,15 @@ def test_tc_sc_14_metrics_include_core_indicators(self, tmp_path): class TestSecurityScan: """Tests for security-scan.mjs JSON output.""" - def test_tc_sc_15_empty_input_returns_zero_findings(self): - """TC-SC-15: Empty stdin returns zero findings.""" + def test_tc_scr_15_empty_input_returns_zero_findings(self): + """TC-SCR-15: Empty stdin returns zero findings.""" data, rc = _run_script("security-scan.mjs", stdin_data="") assert rc == 0 assert data["findings"] == [] assert data["summary"]["total"] == 0 - def test_tc_sc_16_detects_sql_injection_pattern(self, tmp_path): - """TC-SC-16: Detects SQL injection pattern in diff.""" + def test_tc_scr_16_detects_sql_injection_pattern(self, tmp_path): + """TC-SCR-16: Detects SQL injection pattern in diff.""" diff = textwrap.dedent("""\ +++ src/db.js +const result = db.query("SELECT * FROM users WHERE id=" + req.params.id); @@ -405,8 +406,8 @@ def test_tc_sc_16_detects_sql_injection_pattern(self, tmp_path): categories = [f["category"] for f in data["findings"]] assert "A03" in categories, f"Expected A03 (Injection), got: {categories}" - def test_tc_sc_17_output_has_summary_structure(self, tmp_path): - """TC-SC-17: Output has findings/summary/scanned_files keys.""" + def test_tc_scr_17_output_has_summary_structure(self, tmp_path): + """TC-SCR-17: Output has findings/summary/scanned_files keys.""" data, _ = _run_script("security-scan.mjs", stdin_data="no diff here") required_keys = {"findings", "summary", "scanned_files"} missing = required_keys - set(data.keys()) @@ -417,8 +418,8 @@ def test_tc_sc_17_output_has_summary_structure(self, tmp_path): class TestInferVersionBump: """Tests for infer-version-bump.mjs JSON output.""" - def test_tc_sc_18_no_merged_returns_null_bump(self, tmp_path): - """TC-SC-18: No unreleased merged CRs → null bump type.""" + def test_tc_scr_18_no_merged_returns_null_bump(self, tmp_path): + """TC-SCR-18: No unreleased merged CRs → null bump type.""" d = tmp_path / ".devpace" / "backlog" d.mkdir(parents=True) # Only a developing CR, no merged @@ -427,8 +428,8 @@ def test_tc_sc_18_no_merged_returns_null_bump(self, tmp_path): assert rc == 0 assert data["bump_type"] is None - def test_tc_sc_19_merged_feature_suggests_minor(self, tmp_path): - """TC-SC-19: Merged feature CR → minor bump suggestion.""" + def test_tc_scr_19_merged_feature_suggests_minor(self, tmp_path): + """TC-SCR-19: Merged feature CR → minor bump suggestion.""" devpace_dir = _make_devpace(tmp_path) data, rc = _run_script("infer-version-bump.mjs", [devpace_dir, "1.0.0"]) assert rc == 0 @@ -436,8 +437,8 @@ def test_tc_sc_19_merged_feature_suggests_minor(self, tmp_path): assert data["suggested"] == "1.1.0" assert len(data["candidates"]) > 0 - def test_tc_sc_20_output_has_required_structure(self, tmp_path): - """TC-SC-20: Output has current/suggested/bump_type/reasoning/candidates.""" + def test_tc_scr_20_output_has_required_structure(self, tmp_path): + """TC-SCR-20: Output has current/suggested/bump_type/reasoning/candidates.""" devpace_dir = _make_devpace(tmp_path) data, _ = _run_script("infer-version-bump.mjs", [devpace_dir, "1.0.0"]) required_keys = {"current", "suggested", "bump_type", "reasoning", "candidates"} diff --git a/tests/static/test_state_machine.py b/tests/static/test_state_machine.py index 10897a6..eec7039 100644 --- a/tests/static/test_state_machine.py +++ b/tests/static/test_state_machine.py @@ -13,8 +13,8 @@ @pytest.mark.static class TestStateMachine: - def test_tc_sm_01_seven_states_defined(self): - """TC-SM-01: workflow.md defines all 7 states.""" + def test_tc_sm_01_all_states_defined(self): + """TC-SM-01: workflow.md defines all 8 CR states (including optional released).""" content = WORKFLOW_TEMPLATE.read_text(encoding="utf-8") missing = [s for s in CR_STATES if s not in content] assert not missing, f"workflow.md missing states: {missing}" diff --git a/tests/static/test_sync_maintenance.py b/tests/static/test_sync_maintenance.py index c826659..d553e19 100644 --- a/tests/static/test_sync_maintenance.py +++ b/tests/static/test_sync_maintenance.py @@ -1,4 +1,4 @@ -"""TC-SM: Cross-file sync maintenance checks. +"""TC-SYN: Cross-file sync maintenance checks. Detects drift between known cross-file synchronization points: - Command table in devpace-rules.md vs actual skill directories @@ -28,8 +28,8 @@ def _read_text(path): class TestSyncMaintenance: """Cross-file synchronization drift detection.""" - def test_tc_sm_01_command_table_sync(self): - """TC-SM-01: section 0 command table matches skill directories. + def test_tc_syn_01_command_table_sync(self): + """TC-SYN-01: section 0 command table matches skill directories. The '### 命令分层' table in devpace-rules.md lists all skills organised by tier. Every listed name must exist as a skill @@ -65,8 +65,8 @@ def test_tc_sm_01_command_table_sync(self): f"Names in command table but no matching skill directory: {sorted(extra_in_table)}" ) - def test_tc_sm_02_accept_capabilities_sync(self): - """TC-SM-02: accept capability keywords in SKILL.md + rules reference. + def test_tc_syn_02_accept_capabilities_sync(self): + """TC-SYN-02: accept capability keywords in SKILL.md + rules reference. pace-test/SKILL.md (authority) defines 4 fine-grained capabilities for 'accept'. devpace-rules.md section 15 uses a generalized @@ -134,8 +134,8 @@ def test_tc_sm_02_accept_capabilities_sync(self): "(authority delegation) instead of enumerating capabilities" ) - def test_tc_sm_03_schema_files_exist(self): - """TC-SM-03: all expected schema files exist in _schema/ directory. + def test_tc_syn_03_schema_files_exist(self): + """TC-SYN-03: all expected schema files exist in _schema/ directory. The Schema 映射 table was removed from devpace-rules.md §0 (OPT-2 token optimization). Schema files are now discovered @@ -152,8 +152,8 @@ def test_tc_sm_03_schema_files_exist(self): f"Expected schema files missing from _schema/: {sorted(missing)}" ) - def test_tc_sm_04_feature_docs_subcommand_sync(self): - """TC-SM-04: feature docs sub-command list matches SKILL.md. + def test_tc_syn_04_feature_docs_subcommand_sync(self): + """TC-SYN-04: feature docs sub-command list matches SKILL.md. For each skill that has a docs/features/.md, verify that the sub-commands listed in the feature doc match those in SKILL.md. From 9887d61f06640f29c52a4958331d3d2a19ea1a1f Mon Sep 17 00:00:00 2001 From: Sunny <277479420@qq.com> Date: Sat, 14 Mar 2026 01:17:37 +0800 Subject: [PATCH 02/70] =?UTF-8?q?refactor(*):=20=E5=B7=A5=E7=A8=8B?= =?UTF-8?q?=E8=B4=A8=E9=87=8F=E6=B7=B1=E5=BA=A6=E6=95=B4=E6=94=B9=E2=80=94?= =?UTF-8?q?=E2=80=9414=20=E9=A1=B9=E9=97=AE=E9=A2=98=E4=BF=AE=E5=A4=8D=20(?= =?UTF-8?q?H1-H4,=20M1-M10)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 1: 触发准确性修复 - H1: pace-feedback 添加 NOT for 排除条件,消除与 pace-dev 的触发冲突 - H2: pace-learn description 移除"做什么"描述,改为触发关键词列表 - H3: pace-pulse description 移除内部数字(5 checkpoints/30+ minutes) - M6: pace-sync description 统一格式(移除外层双引号、斜杠分隔改逗号) Phase 2: 结构一致性对齐 - H4: pace-feedback 添加 context:fork + agent:pace-engineer - M1: 5 个 Skill 补齐显式 model:sonnet(pace-dev/change/retro/plan/biz) - M5: 统一 14 个 Skill 的 allowed-tools 排列顺序 Phase 3: 测试基础设施 - M2: test_plugin_loading.sh Skill 列表 13→19 - M3: validate-all.sh 新增 Tier 1.9 Hook 测试集成 Phase 4: 文件清理 - M7: verify-procedures.md → test-procedures-verify.md + 更新 8 处引用 - M8: 根目录非标准文件移入 docs/(promt.md→scratch/, principle.md→design/, model.drawio→design/) - M10: pace-role SKILL.md 添加 role-procedures-inference.md 隐式依赖备注 Phase 5: 文档补全 - M4: 补齐 5 个中文特性文档(pace-change/dev/release/review/test) Phase 6: CI 增强 - M9: validate.yml 新增 hooks job(Node.js 20) 验证:pytest 400 pass / 18 skip,model 19/19,NOT for 11/19 Co-Authored-By: Claude Opus 4.6 --- .github/workflows/validate.yml | 28 ++ model.drawio => docs/design/model.drawio | 0 .../design/principles-notes.md | 0 docs/features/pace-change_zh.md | 299 ++++++++++++++++ docs/features/pace-dev_zh.md | 234 +++++++++++++ docs/features/pace-release_zh.md | 330 ++++++++++++++++++ docs/features/pace-review_zh.md | 206 +++++++++++ docs/features/pace-test.md | 4 +- docs/features/pace-test_zh.md | 261 ++++++++++++++ promt.md => docs/scratch/prompt-notes.md | 0 knowledge/_schema/accept-report-contract.md | 4 +- scripts/validate-all.sh | 34 ++ skills/pace-biz/SKILL.md | 3 +- skills/pace-change/SKILL.md | 3 +- skills/pace-dev/SKILL.md | 3 +- skills/pace-feedback/SKILL.md | 6 +- skills/pace-guard/SKILL.md | 2 +- skills/pace-init/SKILL.md | 2 +- skills/pace-learn/SKILL.md | 2 +- skills/pace-plan/SKILL.md | 3 +- skills/pace-pulse/SKILL.md | 4 +- skills/pace-release/SKILL.md | 2 +- skills/pace-retro/SKILL.md | 1 + skills/pace-review/SKILL.md | 2 +- skills/pace-role/SKILL.md | 4 +- skills/pace-sync/SKILL.md | 4 +- skills/pace-test/SKILL.md | 6 +- skills/pace-test/test-procedures-dryrun.md | 2 +- ...rocedures.md => test-procedures-verify.md} | 0 skills/pace-trace/SKILL.md | 2 +- tests/integration/test_plugin_loading.sh | 12 +- 31 files changed, 1435 insertions(+), 28 deletions(-) rename model.drawio => docs/design/model.drawio (100%) rename principle.md => docs/design/principles-notes.md (100%) create mode 100644 docs/features/pace-change_zh.md create mode 100644 docs/features/pace-dev_zh.md create mode 100644 docs/features/pace-release_zh.md create mode 100644 docs/features/pace-review_zh.md create mode 100644 docs/features/pace-test_zh.md rename promt.md => docs/scratch/prompt-notes.md (100%) rename skills/pace-test/{verify-procedures.md => test-procedures-verify.md} (100%) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 76a4360..94de9e2 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -51,3 +51,31 @@ jobs: - name: Run static tests run: pytest tests/static/ -v + + hooks: + name: Hook Tests (Node.js) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Run hook tests + run: | + PASS=0 + FAIL=0 + for f in tests/hooks/test_*.mjs; do + [ -f "$f" ] || continue + if node --test "$f" > /dev/null 2>&1; then + PASS=$((PASS + 1)) + else + echo "FAIL: $f" + node --test "$f" 2>&1 | tail -20 + FAIL=$((FAIL + 1)) + fi + done + echo "Hook tests: $PASS passed, $FAIL failed" + [ "$FAIL" -eq 0 ] diff --git a/model.drawio b/docs/design/model.drawio similarity index 100% rename from model.drawio rename to docs/design/model.drawio diff --git a/principle.md b/docs/design/principles-notes.md similarity index 100% rename from principle.md rename to docs/design/principles-notes.md diff --git a/docs/features/pace-change_zh.md b/docs/features/pace-change_zh.md new file mode 100644 index 0000000..33e8358 --- /dev/null +++ b/docs/features/pace-change_zh.md @@ -0,0 +1,299 @@ +🌐 [English](pace-change.md) | 中文版 + +# 需求变更管理(`/pace-change`) + +在大多数 AI 辅助工作流中,需求变更是一种临时处理的干扰事件——这里加个注释、那里手动改个文件,散落的笔记无人能追溯。devpace 将变更视为**一等公民**。`/pace-change` 提供结构化分诊、多层影响分析、风险量化和可追溯执行,让需求变动不再打乱开发节奏。 + +## 快速开始 + +``` +1. /pace-change add "支持 OAuth 登录" → 分诊 → 影响分析 → 提案 +2. 用户确认提案 → 跨所有项目文件执行变更 +3. 变更记录写入迭代日志 → 完整可追溯性 +``` + +或让 Claude 交互式引导你: + +``` +You: /pace-change +Claude: [基于项目上下文的智能推荐] + 或选择:add / pause / resume / reprioritize / modify / batch +``` + +## 工作流 + +### Step 0:经验预加载 + +在分析开始之前,`/pace-change` 读取 `insights.md` 中与当前变更类型匹配的历史模式。历史数据会在影响报告中引用,回滚模式会提升风险等级。没有历史数据?静默跳过。 + +### Step 1:分诊 + +并非每个变更请求都需要完整分析。在进入影响评估之前,`/pace-change` 将每个请求路由到分诊门禁: + +| 决策 | 含义 | 后续操作 | +|------|------|---------| +| **Accept** | 继续完整分析 | 进入影响分析 | +| **Decline** | 拒绝并记录理由 | 记录原因,结束 | +| **Snooze** | 延迟至满足触发条件 | 持久化记录 + 触发条件,结束。Pulse 在会话启动、新迭代创建和 CR 合并时自动检查触发条件 | + +Claude 根据与当前迭代目标的对齐度、紧急程度和项目方向自动建议分诊决策。Hotfix/关键变更直接跳过分诊。用户始终可以覆盖建议。 + +### Step 2:影响分析(4 层 3 级输出) + +通过分诊的变更进入 **BR 到代码的影响追踪**——与 devpace 从业务需求到代码变更维护的同一条价值链: + +1. **业务需求(BR)层** — 是否影响整个业务目标?成功度量(MoS)是否有风险? +2. **产品功能(PF)层** — 哪些功能受影响?多少需要范围或验收标准变更? +3. **变更请求(CR)层** — 哪些进行中或计划中的 CR 受影响?存在哪些依赖关系? +4. **代码层** — 哪些模块、文件和接口需要修改? + +**三级渐进输出**(对齐 design.md §2): +- **表面层**(默认):1 行结论——"此变更影响 2 个功能,风险为低。" +- **中间层**(追问或中等风险时):3-5 行,包含受影响功能和风险摘要 +- **深入层**(进一步追问或高风险时):完整 4 层追踪、风险矩阵、依赖链,可选 Mermaid 可视化 + +对于中/高风险变更,传递依赖最多追踪 3 层深度(CR-A → CR-B → CR-C)。 + +当变更影响某个 BR 50% 以上的功能(或用户明确指向某个 BR)时,报告升级为 BR 级视图,展示完整的下游级联。 + +### Step 3:风险量化 + 成本估算 + +影响分析之后,`/pace-change` 在三个维度生成半定量风险摘要: + +| 维度 | 低 | 中 | 高 | +|------|:--:|:--:|:--:| +| 受影响模块数 | <=2 | 3-5 | >5 | +| 受影响的进行中 CR | 0 | 1-2 | >=3 | +| 需重置的质量检查 | 0 | 1-3 | >3 | + +综合风险:**低**(所有维度均为低)、**中**(任一为中,无高)或 **高**(任一为高)。高风险变更额外获得建议的测试重点区域和分阶段执行策略。 + +**成本估算**:基于 `insights.md` 中的历史数据(可用时)或启发式规则,附加预估额外工作量以辅助决策。 + +如果 CR 已包含影响分析部分(由 `/pace-test impact` 写入),该数据会被复用而非重新评估。 + +### Step 4:提案、预览与确认 + +Claude 展示具体的调整方案——要创建的新 CR、要转换的状态、要重置的质量检查、迭代容量影响——以及**受影响文件的预览**。用户可请求 `--dry-run` 仅查看预览而不执行。 + +**在你说"执行"之前,不会修改任何内容。** + +### Step 5:执行与记录 + +确认后,所有受影响的项目文件原子化更新: + +1. **CR 文件** — 状态、意图、质量检查、事件日志 +2. **project.md** — 功能树(新条目、暂停/恢复标记、状态变更) +3. **PF 文件** — 验收标准更新 + 历史注解(用于 `modify` 已拆分的功能) +4. **iterations/current.md** — 变更日志条目(迭代跟踪激活时) +5. **state.md** — 当前工作快照和下一步建议 +6. **dashboard.md** — 变更管理指标(增量更新) +7. **git commit** — 所有变更记录在单个可追溯的提交中 + +### Step 6:下游引导 + 外部同步 + +执行后,`/pace-change` 提供**按类型区分的下一步引导**: + +| 变更类型 | 引导 | +|---------|------| +| add | "开始开发?" → 确认后进入 `/pace-dev` | +| modify | "N 个检查需要重新验证,继续?" → 恢复 `/pace-dev` | +| pause | "调整迭代范围?" → 确认后进入 `/pace-plan adjust` | +| resume | "继续开发?" → 确认后进入 `/pace-dev` | +| reprioritize | "切换到新的开始工作?" → 确认后进入 `/pace-dev` | + +同时检查外部 Issue 关联并在需要时生成同步摘要——保持你的 GitHub Issues(或其他工具)一致,不产生自动副作用。 + +**容量协调**:`add` 导致迭代容量溢出后,Claude 直接询问是否调整范围(内联 `/pace-plan adjust`)。`pause` 释放容量后,建议拉入等待中的功能。 + +## 变更类型 + +| 类型 | 语法 | 描述 | +|------|------|------| +| **add** | `/pace-change add <描述>` | 插入新需求。创建 PF + CR 条目,评估迭代容量。 | +| **pause** | `/pace-change pause <功能>` | 暂停功能。所有关联 CR 转为 `paused`,保留先前状态,解除依赖阻塞。功能树显示暂停标记。 | +| **resume** | `/pace-change resume <功能>` | 恢复已暂停的功能。CR 恢复到暂停前的状态。质量检查根据暂停期间的代码变更重新验证。 | +| **reprioritize** | `/pace-change reprioritize <描述>` | 调整优先级顺序。更新下一步推荐并重排迭代计划。 | +| **modify** | `/pace-change modify <功能> <变更>` | 变更现有功能的范围或验收标准。受影响的质量检查基于敏感度范围匹配精确重置。PF 文件添加历史注解。 | +| **batch** | `/pace-change batch <描述>` | 一次执行多个变更。合并影响分析、交叉影响检测、单次确认、单次 git commit。 | +| **undo** | `/pace-change undo` | 撤销上一次 `/pace-change` 操作(仅限当前会话)。使用 `git revert` 进行精确回滚。 | +| **history** | `/pace-change history [功能\|--all\|--recent N]` | 查询变更历史,从迭代日志、CR 事件、PF 注解和 git log 聚合。 | +| **apply** | `/pace-change apply <模板>` | 应用 `.devpace/rules/change-templates.md` 中预定义的变更模板。 | + +### 快速引用 + +- `#N` — 按编号引用 CR(如 `pause #3` = 暂停 CR-003 的功能) +- `--last` — 应用到最近工作的 CR(如 `modify --last`) +- `--dry-run` — 仅预览,不执行 + +省略类型参数时启动**智能引导对话**,Claude 扫描项目上下文(已暂停的 CR、Snooze 触发条件、迭代容量、频繁变更的功能)并给出个性化推荐,然后回退到标准选项列表。 + +## 核心特性 + +### 上下文感知智能引导 + +无参数调用时,`/pace-change` 扫描项目状态并推荐最可能的操作——"恢复已暂停的功能 X?"或"评估之前 Snooze 的变更 Y?"——然后再展示标准选项列表。 + +### 三级渐进输出 + +影响报告遵循表面层 → 中间层 → 深入层的渐进模式。日常低风险变更仅显示 1 行;复杂变更在追问后展开。典型变更的阅读量从 15-20 行降低到 3-5 行。 + +### 精确质量检查重置 + +修改需求时,`/pace-change` 将变更范围与每个质量检查的 `sensitivity` 字段(`checks.md` 中的 glob 模式)比较。仅重置范围重叠的检查——不再过度重置或遗漏重置。 + +### 批量变更 + +一次处理多个变更:合并影响分析、交叉影响检测(如暂停 A 而 B 依赖 A)、单次确认、单次 git commit。支持显式 `/pace-change batch` 和自然语言("暂停 A 和 B")。 + +### 变更撤销 + +基于 git commit 历史的精确回滚。在迭代变更日志中追加"undo"条目。限于当前会话以防止跨会话状态不一致。 + +### 变更历史查询 + +从四个分散来源(迭代日志、CR 事件、PF 注解、git log)聚合变更历史为统一时间线。功能被变更超过 3 次时主动发出警告。 + +### Snooze 主动提醒 + +Snooze 的变更以触发条件持久化保存。Pulse 系统在会话启动、新迭代创建和 CR 合并时自动检查条件——确保延迟的变更不会被遗忘。每个 Snooze 项仅提醒一次。 + +### 经验驱动分析 + +`insights.md` 中的历史模式为影响分析提供信息("类似变更历史上影响了 N 个模块"),并为曾需要回滚的模式提升风险等级。 + +### 传递依赖追踪 + +影响分析追踪最多 3 层传递依赖,显示深度随风险等级缩放。直接、间接和深层影响通过缩进展示。 + +### 下游流自动化 + +执行后,按类型区分的引导帮助用户无缝过渡到下一步——开始开发、重新验证检查或调整迭代范围——通过一键确认进入相关 Skill。 + +### 结构化分诊路由 + +每个变更请求在消耗分析工作量之前都经过 Accept/Decline/Snooze 路由。防止低价值或不成熟的请求干扰活跃工作,同时确保没有请求被静默丢弃。 + +### BR 级影响视图 + +当变更大到影响整个业务需求时,影响报告自动从功能级升级到 BR 级,展示完整级联:业务目标影响、成功度量风险、所有受影响功能和所有受影响 CR。 + +### 影响可视化(Mermaid) + +对于中/高风险变更(或应请求),Mermaid 图表可视化影响链——从变更到受影响功能再到单个 CR。低风险变更跳过可视化以避免噪音。 + +### 变更成本估算 + +风险量化包含预估工作量维度——基于 `insights.md` 中的历史检查点计数,或在无历史数据时使用启发式规则。 + +### 外部同步检查 + +任何状态变更操作后,`/pace-change` 为外部关联的 CR 生成同步摘要并在需要时提示同步——保持你的 GitHub Issues(或其他工具)一致,不产生自动副作用。 + +### 变更管理度量指标 + +执行数据输入 `metrics.md` 变更管理指标(频率、类型分布、返工率、分诊分布、执行时间),在 `dashboard.md` 中增量更新。 + +## 降级模式 + +当 `.devpace/` 未初始化时,`/pace-change` 仍可工作——它优雅降级而非失败: + +- **分诊** 简化为口头确认:"现在处理还是记下来以后再说?" +- **影响分析** 使用 `Glob`/`Grep`/`Read` 直接扫描代码库,分析 `import`/`require` 依赖图,识别受影响模块和风险区域 +- **Git 历史增强**:共变分析(频繁共同修改的文件组)和热点识别(近期变更热点) +- **风险量化** 从依赖图评估模块扩散;CR 和质量检查维度标记为"不可用" +- **执行** 仅操作代码——不创建或修改 `.devpace/` 文件 +- **增量丰富**:分析结果可选择性持久化到 `.devpace-lite/changes.md`。累计 3 次以上降级使用后,升级提示变得更具体 +- **完成时** 自然提及:"完整初始化可启用持久化追溯、质量门禁和度量指标。运行 `/pace-init` 开始。" + +这意味着你可以在任何项目、任何时间使用 `/pace-change`,即使尚未决定完全采用 devpace。 + +## 使用场景 + +### 场景 1:迭代中途插入新需求 + +利益相关者在团队专注于核心 API 开发时要求支持 OAuth 登录。 + +``` +You: /pace-change add "通过 Google 和 GitHub 支持 OAuth 登录" +Claude: 分诊:这是一个与当前 API 冲刺无关的新功能。 + 建议:Snooze 至 API 里程碑完成。 + Accept / Decline / Snooze? + +You: Accept——客户演示需要。 +Claude: 此变更新增 1 个功能,风险为低。 + [追问:"具体是什么?"] + 提案:创建 2 个 CR,将"限流"延后到下一迭代。 + 迭代容量超出——现在调整范围?(Y/N) + +You: Yes +Claude: 完成。2 个 CR 已创建,迭代计划已更新,限流已延后。 + 开始开发? → /pace-dev +``` + +### 场景 2:批量暂停和重排优先级 + +``` +You: /pace-change batch "暂停通知功能,将导出排在搜索前面" +Claude: 检测到 2 个变更意图: + ① pause:通知系统 + ② reprioritize:导出排在搜索前面 + 是否正确? + +You: 是 +Claude: 合并影响:3 个 CR 暂停(通知),1 个 CR 重排序。 + 未检测到交叉影响。风险:低。 + 继续? + +You: 执行 +Claude: 完成。通知已暂停(3 个 CR 保留),导出已提升优先级。 + 开始处理导出? → /pace-dev +``` + +### 场景 3:修改并精确重置检查 + +``` +You: /pace-change modify search "响应时间必须在 200ms 以内,而不是 500ms" +Claude: 此变更影响 1 个功能,风险为中。 + CR-005(搜索实现):Gate 1 已通过,3 个检查中有 1 个需要重置。 + [精确匹配:"performance test" 检查的敏感度范围为 "src/search/", + 与变更重叠。"lint" 和 "type check" 保留。] + 继续? + +You: 是 +Claude: 完成。CR-005 的 1 个检查已重置。历史注解已添加。 + 1 个检查需要重新验证——继续开发? +``` + +### 场景 4:撤销上次变更 + +``` +You: /pace-change undo +Claude: 上次变更(2 分钟前):暂停了"通知系统"(3 个 CR)。 + 撤销此变更? + +You: 是 +Claude: 已撤销。通知系统已恢复,3 个 CR 回到之前的状态。 +``` + +## 与其他命令的集成 + +| 命令 | 关系 | +|------|------| +| `/pace-dev` | 变更执行后,`/pace-dev` 接手新建或修改的 CR 进行实现。已暂停的 CR 从开发队列中排除。下游引导提供一键过渡。 | +| `/pace-plan` | `/pace-change add` 创建的 PF 和 CR 条目成为计划的一部分。容量溢出时触发内联 `adjust` 协调。重排优先级的调整反映在 `/pace-plan` 的迭代视图中。 | +| `/pace-sync` | 状态变更操作后,`/pace-change` 生成同步摘要并提示推送同步。 | +| `/pace-test` | `modify` 重置的质量检查(通过敏感度范围精确匹配)在下次 `/pace-test` 门禁审查时重新评估。`/pace-test impact` 的影响数据在 `/pace-change` 风险量化中被复用。 | +| `/pace-status` | 反映 `/pace-change` 做出的所有变更——已暂停的功能、新 CR、更新的优先级。 | +| `/pace-pulse` | Snooze 唤醒检查由 Pulse 在会话启动、新迭代和 CR 合并时执行。变更预警信号(验收漂移、重复失败、需求冲突)是 Pulse 信号。 | +| `/pace-retro` | 变更管理指标(频率、类型分布、返工率)在回顾报告中汇总。 | + +## 相关资源 + +- [用户指南 — /pace-change 部分](../user-guide.md) — 快速参考 +- [设计文档 — 变更管理](../design/design.md) — 架构和设计原则 +- [skills/pace-change/](../../skills/pace-change/) — 操作规程(按步骤拆分:common、triage、impact、risk、execution、types;按子命令拆分:batch、undo、history、apply、degraded) +- [cr-format.md](../../knowledge/_schema/cr-format.md) — CR 文件 Schema(包含 `paused` 状态定义) +- [checks-format.md](../../knowledge/_schema/checks-format.md) — 质量检查 Schema(包含敏感度范围) +- [metrics.md](../../knowledge/metrics.md) — 变更管理度量指标定义 +- [devpace-rules.md](../../rules/devpace-rules.md) — 运行时行为规则 diff --git a/docs/features/pace-dev_zh.md b/docs/features/pace-dev_zh.md new file mode 100644 index 0000000..cc8a17c --- /dev/null +++ b/docs/features/pace-dev_zh.md @@ -0,0 +1,234 @@ +# 开发工作流(`/pace-dev`) + +`/pace-dev` 是 devpace 的核心开发 Skill。它驱动变更请求(CR)走完完整生命周期——从意图澄清到代码实现再到质量门禁——在一个自主工作流中完成。Claude 进入"推进模式",编写代码、运行测试、遇到失败时自修正,并在每个有意义的步骤进行 commit。该 Skill 根据变更的复杂度自适应调整严格程度:单文件拼写修复走最小化流程,多模块功能则生成完整的执行计划并征求用户确认。 + +## 快速开始 + +``` +1. /pace-dev "add user login" --> 定位或创建 CR,澄清意图,开始编码 +2. (Claude 自主工作) --> 实现、测试、提交,运行 Gate 1 和 Gate 2 +3. "LGTM" --> 人工审批(Gate 3)--> CR 合并 +``` + +发出初始命令后,Claude 自主驱动工作流,无需额外 prompt,直到需要你做决策或到达人工审批门禁。 + +## CR 生命周期概览 + +每项变更都经历六状态生命周期。`/pace-dev` 自动管理状态转换;你只需在 Gate 3(人工审批)介入。 + +``` +created --> developing --> verifying --> in_review --> approved --> merged + | | | | | | + | 意图 | 代码 & | Gate 1 | Gate 2 | Gate 3 | 合并后 + | 检查点 | 测试 | (代码 | (需求 | (人工) | 更新 + | | | 质量) | 质量) | | +``` + +| 状态 | 发生什么 | 谁执行 | +|------|---------|--------| +| `created` | CR 已创建,包含标题和意图;完成复杂度评估 | Claude | +| `developing` | 代码实现、测试编写、每步 git commit | Claude | +| `verifying` | Gate 1——自动化代码质量检查;失败时自修正 | Claude | +| `in_review` | Gate 2——与验收标准对比;生成审查摘要 | Claude | +| `approved` | Gate 3——人工审查 diff 摘要并批准 | 你 | +| `merged` | 分支合并,state.md 更新,关联 PF 刷新 | Claude | + +## 核心特性 + +### 意图检查点 + +CR 首次进入 `developing` 时,Claude 执行一个自准备步骤来锁定范围和验收标准。这不是需要你填写的表单——Claude 在内部完成并告知你"范围已确认,开始工作"。 + +- **简单(S)**:记录你的原始请求及一条自由文本验收条件。 +- **标准(M)**:增加编号验收标准列表,用 `[TBD]` 标签标注歧义项。 +- **复杂(L/XL)**:生成完整的 Given/When/Then 验收标准、执行计划,并向你提最多 2 个澄清问题(每个附带推荐答案)。 + +如果你问"计划是什么?",Claude 会展示完整的意图部分,包括执行计划。 + +### 复杂度评估 + +Claude 从四个维度评估每个 CR 并分配大小:S、M、L 或 XL。 + +| 维度 | S | M | L | XL | +|------|---|---|---|-----| +| 涉及文件数 | 1-3 | 4-7 | 8-15 | >15 | +| 涉及目录数 | 1 | 2-3 | 4-5 | >5 | +| 验收标准数 | 1 | 2-3 | 4+ | 多组 | +| 跨模块依赖 | 无 | 单向 | 双向 | 架构级 | + +最高维度决定最终评级。L/XL 级 CR 自动进入拆分评估流程,Claude 会建议将工作拆成更小的 CR。 + +### 自适应路径 + +复杂度决定工作流的仪式化程度: + +| 路径 | 复杂度 | 行为 | +|------|--------|------| +| **Quick** | S,单文件 | 最小意图记录,无执行计划,直接编码 | +| **Standard** | S 多文件,M | 标准意图 + 编号标准;可选执行计划 | +| **Full** | L,XL | 完整意图 + 强制执行计划 + 计划反思 + 编码前用户确认门禁 | + +**升级守卫**在意图检查点期间监视范围蔓延。如果一个 S 级 CR 实际涉及多个模块,Claude 会建议升级到 M 或 L。该建议不会阻断流程——你可以选择继续使用原级别。 + +### 执行计划(L/XL) + +对于复杂变更,Claude 在编写任何代码之前生成分步计划: + +- 每步是一个原子操作,对应一次有意义的 commit。 +- 步骤包含确切的文件路径、要执行的操作和可验证的预期结果。 +- 步骤间的依赖关系被显式标注。 +- 当测试策略存在时,测试骨架步骤排在实现步骤之前。 + +生成计划后,Claude 从四个维度执行**计划反思**(需求覆盖度、过度工程风险、拆分必要性、技术假设),记录 1-3 行观察。然后将计划呈现给你确认后再开始编码。 + +### 门禁反思 + +每个质量门通过后,Claude 在 CR 事件日志中追加简短的自评: + +- **Gate 1 反思**:技术债观察、测试覆盖评估、测试先行遵从度审查。 +- **Gate 2 反思**:边界场景覆盖和验收完整度观察。 + +这些反思不会阻断工作流。它们积累质量信号,在 CR 合并时馈入经验提取。 + +### 漂移检测 + +两个互补的监控在每个检查点(git commit + CR 更新)运行: + +- **意图漂移**:将变更文件与声明的范围对比。如果超过 30% 的文件落在意图边界之外,Claude 会标记:"这些文件超出声明范围——是有意扩展还是范围蔓延?" +- **复杂度漂移**:将实际文件/目录数量与初始复杂度阈值对比。如果一个 S 级 CR 已触及 4+ 个文件,Claude 会建议升级。每个 CR 最多标记一次。 + +两种检测均为建议性质——它们不会阻断你的工作流。 + +### PF 溢出检查 + +当 CR 被创建或合并时,Claude 检查关联的产品功能(PF)是否已超出 `project.md` 中的内联容量: + +- **触发条件**:功能规格超过 15 行、3+ 个关联 CR,或之前对该 PF 执行过 `/pace-change modify`。 +- **动作**:自动将 PF 提取为 `.devpace/features/PF-xxx.md` 下的独立文件,并在 `project.md` 中更新为链接。 +- **零摩擦**:无需确认。提取会在执行摘要中报告。 + +### 快速 CR 切换 + +有多个进行中的 CR?快速切换: + +- `/pace-dev #3`——按编号直接跳转到 CR-003。 +- `/pace-dev --last`——恢复最近工作过的 CR。 +- `/pace-dev "login"`——按关键词匹配(已有行为)。 + +### 简化审批 + +简化审批路径(跳过 `in_review` 等待)现在区分外观性修复和结构性修复: + +- **外观性修复**(lint、格式化、import 排序)不计为"非首次通过"。这意味着更多 S 级 CR 有资格走简化审批。 +- **结构性修复**(逻辑错误、类型错误、缺少 return)仍然取消简化审批资格。 +- **批量审批**:当 2+ 个 CR 同时处于 `in_review` 且均符合简化审批条件时,Claude 提供批量确认提示。 + +### L/XL 步级进度 + +对于有执行计划的 L/XL 级 CR,每完成一步会产出: + +1. CR 事件日志中的步骤检查点标记(`[checkpoint: step-3-done]`)。 +2. state.md 中的步级定位器(`→ Step 3/5: middleware implementation`)。 +3. 给用户的一行进度通知:`[Step 3/5] middleware implementation complete`。 + +这实现了精确的跨会话恢复——下次会话从准确的步骤处继续。 + +### 执行计划编辑 + +在 L/XL 级 CR 的确认门禁期间,你可以用自然语言调整计划: + +- "删除步骤 3" / "合并步骤 4 和 5" / "在步骤 2 后添加一步:..." / "交换步骤 3 和 4" +- Claude 应用变更并重新呈现更新后的计划供确认。 +- 你还可以设置**暂停点**:"在步骤 3 后暂停"——Claude 会在那里停下并输出进度摘要,等待确认后再继续。 + +### 探索到推进的上下文继承 + +在同一会话中从探索模式切换到推进模式时,Claude 自动从先前讨论中提取相关上下文: + +- 方案选择及其理由 +- 发现的约束和依赖 +- 探索中达成的结论 + +该上下文会带来源标注预填到 CR 意图部分,无需重新解释已做出的决策。 + +### 开发中压缩建议 + +对于已完成执行计划 60% 以上的 L/XL 级 CR,当当前步骤与之前步骤无关时,Claude 可能建议 `/compact`——为剩余工作释放上下文空间。 + +### 推进中内联探索 + +如果你在实现过程中需要思考或讨论替代方案("让我想想..." / "有没有更好的方法?"),Claude 暂停推进工作流进行自由讨论,然后在你说"继续"时无缝恢复。讨论成果自动更新到 CR 意图中。 + +## 使用场景 + +### 场景 1:快速修复 Bug + +``` +你: /pace-dev "fix null check in auth middleware" +Claude: 未找到现有 CR。创建 CR-012(类型: defect,严重度: medium)。 + 范围已确认,开始工作。 + + ...(实现修复、添加测试、提交)... + + 变更摘要 + - 文件: 修改 1 / 新增 1 + - CR 状态: created --> developing --> verifying --> in_review + - Gate 1: 通过。Gate 2: 通过。 + 准备好供你审查。批准合并? +``` + +### 场景 2:多模块功能(L) + +``` +你: /pace-dev "add role-based access control" +Claude: CR-013 已创建。复杂度: L(8 文件、4 目录、双向依赖)。 + 建议拆分为 2 个 CR: + - CR-013a: 角色模型 + 权限中间件 + - CR-013b: 角色分配的管理界面 + 依赖关系: CR-013a 先于 CR-013b。 + 拆分还是作为一个 CR 继续? + +你: 拆分。 +Claude: 已创建 CR-013a 和 CR-013b。开始 CR-013a。 + 以下是执行计划(5 步): + 1. 在 models/role.ts 中定义 Role schema ... + ... + 确认开始,还是调整? + +你: 开始吧。 +Claude: ...(自主实现,带检查点)... +``` + +### 场景 3:恢复中断的工作 + +``` +你: /pace-dev +Claude: 恢复 CR-010(状态: developing,执行计划步骤 3/5)。 + 上次检查点: "已完成 API 路由,下一步: 编写集成测试。" + 从步骤 4 继续。 +``` + +## 与其他命令的集成 + +| 命令 | 与 `/pace-dev` 的关系 | +|------|----------------------| +| `/pace-init` | 初始化 `/pace-dev` 运行所在的 `.devpace/` 项目结构 | +| `/pace-review` | CR 到达 `in_review` 时自动调用;生成 Gate 2 的 diff 摘要 | +| `/pace-test` | 开发期间可调用,生成与验收标准对齐的测试骨架 | +| `/pace-change` | 处理需求变更(增删改 PF);`/pace-dev` 负责实现 | +| `/pace-sync` | CR 状态转换后,sync-push hook 提醒你推送状态到 GitHub | +| `/pace-status` | 随时查看当前 CR 状态和项目进度 | +| `/pace-guard` | 在 L/XL 级 CR 的意图检查点期间调用风险预扫描 | +| `/pace-next` | CR 合并后或无进行中工作时建议下一步操作 | + +## 相关资源 + +- [dev-procedures-common.md](../../skills/pace-dev/dev-procedures-common.md) -- 通用规则(context.md 生成、同步建议、决策日志、透明度摘要) +- [dev-procedures-intent.md](../../skills/pace-dev/dev-procedures-intent.md) -- 意图检查点、复杂度评估、执行计划、方案确认 +- [dev-procedures-developing.md](../../skills/pace-dev/dev-procedures-developing.md) -- 步骤隔离、漂移检测、L/XL 检查点 +- [dev-procedures-gate.md](../../skills/pace-dev/dev-procedures-gate.md) -- Gate 1/2 通过后反思 +- [dev-procedures-postmerge.md](../../skills/pace-dev/dev-procedures-postmerge.md) -- 功能发现、PF 溢出检查 +- [dev-procedures-defect.md](../../skills/pace-dev/dev-procedures-defect.md) -- 缺陷/热修复 CR 创建及修复后处理 +- [cr-format.md](../../knowledge/_schema/cr-format.md) -- CR 文件 schema(字段、状态、事件日志格式) +- [devpace-rules.md](../../rules/devpace-rules.md) -- 运行时行为规则(推进模式约束、双模式系统) +- [用户指南](../user-guide.md) -- 所有命令快速参考 diff --git a/docs/features/pace-release_zh.md b/docs/features/pace-release_zh.md new file mode 100644 index 0000000..fd1ce54 --- /dev/null +++ b/docs/features/pace-release_zh.md @@ -0,0 +1,330 @@ +# 发布管理(`/pace-release`) + +devpace 将发布视为一项**主动编排活动**,而非被动的状态追踪。`/pace-release` 驱动完整的发布生命周期——收集已合并的变更、跨环境部署、验证结果、以及通过自动化 changelog、版本号管理和标签完成发布关闭——全程基于 `git` 和 `gh` 等标准工具。它编排你的发布流水线,而非替代你的 CI/CD。 + +## 前置条件 + +| 条件 | 用途 | 是否必需? | +|------|------|:---------:| +| `.devpace/` 已初始化 | 包含已合并 CR 的核心 devpace 项目结构 | 是 | +| `.devpace/releases/` 目录 | 发布文件存储(首次 `create` 时自动创建) | 自动 | +| `integrations/config.md` | Gate 4 检查、部署命令、版本文件配置、环境晋升 | 可选 | +| `gh` CLI | 通过 `tag` 子命令创建 GitHub Release | 可选 | + +> **优雅降级**:所有功能在没有 `integrations/config.md` 时都能正常工作——只是需要手动提供更多信息。Changelog 生成始终可用,因为它直接读取 CR 元数据。 + +## 快速开始 + +``` +1. /pace-release create --> 收集已合并的 CR,建议版本号,创建 REL-001(staging) +2. /pace-release deploy --> 记录部署到目标环境(deployed) +3. /pace-release verify --> 执行验证清单(verified) +4. /pace-release close --> 生成 changelog + 升级版本号 + 创建标签 + 级联更新(closed) +``` + +或者直接调用 `/pace-release`(无参数)——引导向导会检测当前状态并引导你执行正确的下一步。 + +## 命令参考 + +### 用户层(User Layer) + +以下六个命令覆盖标准发布生命周期。大多数团队只需要这些命令。 + +#### `create` + +从已合并的 CR 创建新的 Release。 + +**语法**:`/pace-release create` + +扫描 `.devpace/backlog/` 中处于 `merged` 状态且尚未关联到 Release 的 CR。按类型排序显示候选项(hotfix > defect > feature),询问包含哪些 CR,建议语义化版本号,并创建一个处于 `staging` 状态的 `REL-xxx.md` 文件。如果存在 `integrations/config.md`,可选执行 Gate 4 系统级检查。详见 [release-procedures-create.md](../../skills/pace-release/release-procedures-create.md)。 + +#### `deploy` + +记录一次环境部署。 + +**语法**:`/pace-release deploy` + +支持单环境和多环境晋升。在多环境配置下,遵循定义的晋升路径(`env1 -> env2 -> ... -> envN`),在每个环境执行 deploy + verify 后再晋升到下一个环境。将部署记录追加到 Release 文件,并将状态从 `staging` 转换为 `deployed`。详见 [release-procedures-deploy.md](../../skills/pace-release/release-procedures-deploy.md)。 + +#### `verify` + +执行部署后验证。 + +**语法**:`/pace-release verify` + +展示验证清单(当 `integrations/config.md` 定义了验证命令时,自动验证结果会预填充)。引导你逐项确认。如果发现问题,记录问题并帮助创建与当前 Release 关联的 defect/hotfix CR。全部通过后,将状态转换为 `verified`。详见 [release-procedures-verify.md](../../skills/pace-release/release-procedures-verify.md)。 + +#### `close` + +执行所有关闭操作,完成发布。 + +**语法**:`/pace-release close` + +要求 `verified` 状态。自动执行完整的关闭链:changelog 生成、版本文件升级、Git 标签创建(每一步都会显示简要提示,可跳过),然后进行级联状态更新——CR 状态更新为 `released`、project.md 功能树标记、迭代追踪、state.md 清理和仪表盘指标更新。详见 [release-procedures-close.md](../../skills/pace-release/release-procedures-close.md) 中的 8 步关闭链。 + +#### `full` + +`close` 的推荐别名,语义更清晰("完成发布"而非"关闭")。 + +**语法**:`/pace-release full` + +行为与 `close` 完全一致。详见 [release-procedures-close.md](../../skills/pace-release/release-procedures-close.md)。 + +#### `status` + +查看当前 Release 状态和建议的下一步操作。 + +**语法**:`/pace-release status` + +显示活跃 Release 的 CR 按类型分组明细、部署问题计数、验证进度,以及推荐的下一步操作。当不存在活跃 Release 时,显示可供发布的已合并 CR 数量。详见 [release-procedures-status.md](../../skills/pace-release/release-procedures-status.md)。 + +### 专家层(Expert Layer) + +以下命令可单独使用,供需要精细控制特定发布步骤的团队使用。在正常的 `close` 流程中,步骤 1-3 会自动执行。 + +#### `changelog` + +从 CR 元数据自动生成 CHANGELOG.md。 + +**语法**:`/pace-release changelog` + +读取活跃 Release 中包含的 CR,按类型分组(Features / Bug Fixes / Hotfixes)并关联 PF,将条目写入 Release 文件和项目根目录的 `CHANGELOG.md`。详见 [release-procedures-changelog.md](../../skills/pace-release/release-procedures-changelog.md)。 + +#### `version` + +升级语义化版本号。 + +**语法**:`/pace-release version` + +从 `integrations/config.md` 读取版本文件配置(支持 JSON、TOML、YAML、纯文本)。根据 CR 类型推断升级级别:包含 feature = minor,仅 defect/hotfix = patch。用户可覆盖。就地更新版本文件。详见 [release-procedures-version.md](../../skills/pace-release/release-procedures-version.md)。 + +#### `tag` + +创建 Git 标签,可选创建 GitHub Release。 + +**语法**:`/pace-release tag` + +使用 Release 版本号和配置的前缀(默认 `v`)创建带注释的 Git 标签。当 `gh` CLI 可用时,提供创建 GitHub Release 的选项,以 changelog 内容作为 Release Notes。详见 [release-procedures-tag.md](../../skills/pace-release/release-procedures-tag.md)。 + +#### `notes` + +生成面向用户的 Release Notes,按业务影响组织。 + +**语法**:`/pace-release notes [--role biz|ops|pm]` + +与面向开发者的 changelog(按 CR 类型分组)不同,Release Notes 按 BR(业务需求)和 PF(产品功能)组织,使用产品语言,不包含技术标识符。包含"业务影响"章节,向上追溯到 OBJ 级别的目标和 MoS 进度。 + +通过 `--role` 参数生成特定角色视角的通知:`biz`(面向管理层的业务影响报告)、`ops`(面向运维的部署手册)、`pm`(面向产品经理的功能交付清单)。详见 [release-procedures-notes.md](../../skills/pace-release/release-procedures-notes.md)。 + +#### `branch` + +管理发布分支。 + +**语法**:`/pace-release branch [create|pr|merge]` + +支持 `integrations/config.md` 中配置的三种分支模式:直接发布(默认,在 main 上打标签)、发布分支(`release/v{version}` 用于最终修复)、Release PR(PR 驱动的发布流程,灵感来自 Release Please)。未配置分支模式时,所有操作在 main 分支上进行。详见 [release-procedures-branch.md](../../skills/pace-release/release-procedures-branch.md)。 + +#### `rollback` + +当已部署的 Release 出现严重问题时记录回滚。 + +**语法**:`/pace-release rollback` + +仅在 Release 处于 `deployed` 状态时可用。记录回滚原因,将回滚条目追加到部署日志,将状态转换为 `rolled_back`(终态),并引导创建根因追踪的 defect/hotfix CR。在回滚后创建新 Release 时,已回滚 Release 中无问题的 CR 会自动预填充为候选项,减少重复选择。详见 [release-procedures-rollback.md](../../skills/pace-release/release-procedures-rollback.md)。 + +#### `status history` + +查看发布历史时间线及 DORA 趋势。 + +**语法**:`/pace-release status history` + +扫描所有 Release 文件生成跨发布纵向视图:版本演进、每个 Release 的 CR 数量/类型、回滚标记、平均发布周期时长,以及 DORA 指标趋势(部署频率、前置时间、变更失败率)。默认显示最近 10 个 Release。详见 [release-procedures-status.md](../../skills/pace-release/release-procedures-status.md)。 + +## 发布状态机 + +``` + create deploy verify close + (merged CRs) -----> staging -----> deployed -----> verified -----> closed + | + | rollback + v + rolled_back +``` + +| 状态 | 含义 | 允许的转换 | +|------|------|-----------| +| `staging` | Release 已创建,CR 已收集,准备部署 | `deployed` | +| `deployed` | 已部署到目标环境 | `verified`、`rolled_back` | +| `verified` | 部署后验证通过 | `closed` | +| `closed` | 发布完成,所有关闭操作已执行(终态) | -- | +| `rolled_back` | 因严重问题回退部署(终态) | -- | + +`deployed` 和 `verified` 转换需要人工确认。`verified` 到 `closed` 的转换由 Claude 自动执行(包括关闭链)。 + +## 核心特性 + +### Gate 4:系统级发布检查 + +在 `create` 之后运行的可选预部署门禁: + +1. **构建验证**——执行 `integrations/config.md` 中的构建命令;失败时显示最后 10 行错误输出及建议修复步骤 +2. **CI 状态检查**——查询 CI 流水线状态(无显式配置时自动检测 CI 配置);失败时通过 `gh run view --web` 提供 CI 运行 URL +3. **候选项完整性**——确认所有包含的 CR 已通过 Gate 1/2/3(`merged` 状态);失败时列出具体 CR 及其未通过的 Gate +4. **测试报告**——通过 `/pace-test report` 自动生成 Release 级质量报告 + +Gate 4 不会阻断 Release 创建——它在部署前暴露问题。检查结果持久化到 Release 文件中用于审计追溯。 + +### CR 依赖检测 + +在 `create` 过程中自动检测候选 CR 之间的依赖关系: +- **功能依赖**:关联到同一 PF 的 CR 被标记为功能相关 +- **代码级依赖**:修改相同文件的 CR 被标记为代码交叉风险 +- 显示包含/排除建议的依赖关系图 + +### 发布就绪检查 + +`create` 过程中的可选预验证,扫描候选 CR 的代码变更: +- 临时代码标记(`TODO`、`FIXME`、`console.log`、`debugger`) +- 缺失的测试覆盖(没有 accept 记录的 CR) +- 生成就绪度评分(A/B/C)——仅作参考,不会阻断流程 + +### 发布影响预览 + +在 `create` 之后自动生成,提供发布级别的全景视图: +- 代码变更统计(新增/删除行数、影响文件数) +- 模块级变更热力图 +- 风险区域高亮(多个 CR 修改同一文件) +- 业务影响追溯(本次 Release 对 OBJ/BR 进度的贡献) + +### Changelog 自动生成 + +Changelog 条目完全从 CR 元数据(标题、类型、PF 关联)生成,无需手动编写。输出同时写入 Release 文件和项目根目录的 `CHANGELOG.md`(在顶部追加,保留历史记录)。 + +### 带业务影响的 Release Notes + +与 changelog 不同的独立输出:按 BR/PF 组织,使用产品语言,包含"业务影响"章节,向上追溯到 OBJ 级别的目标和 MoS 里程碑。通过 `--role` 参数支持角色视角: +- `--role biz`:面向业务(OBJ 进度、MoS 达成情况) +- `--role ops`:面向运维(部署详情、风险评估、回滚方案) +- `--role pm`:面向产品(功能交付清单、完成百分比) + +Release Notes 生成门槛已降低:当 Release 包含至少 1 个 feature CR 时即可生成(之前要求 2+ 个 CR)。 + +### 带全景视图的环境晋升 + +当 `integrations/config.md` 定义了多个环境时,`deploy` 按顺序晋升路径执行,每个环境进行 deploy + verify。每次 deploy/verify 操作都会显示完整的晋升全景图及当前位置:`[staging ✅] → [canary ✅] → [production 👈 current] → [done]`。 + +### 关闭进度追踪 + +8 步关闭链逐步显示进度(`[N/8] ✅ Step name`),并支持中断恢复——如果任何步骤失败,之前的步骤会被保留,流程可以从失败步骤恢复。 + +### 带候选项预填充的回滚路径 + +当已部署的 Release 出现严重问题时,`rollback` 提供结构化的应对方案:记录事件、回退到已知状态、创建 CR 追踪修复。在回滚后创建新 Release 时,已回滚 Release 中无问题的 CR 会自动预填充为候选项,减少重复选择。 + +### 带回滚追踪的引导向导 + +无参数向导现在会追踪已回滚的 Release,并在引导下一步操作前显示修复 CR 的状态。防止用户遗忘未解决的回滚后续事项。 + +### 发布历史时间线 + +`status history` 提供跨发布纵向视图:版本演进、每个 Release 的 CR 数量/类型、回滚标记,以及 DORA 趋势摘要。 + +### 发布调度 + +可选的发布节奏配置(`integrations/config.md`)支持在向导和 pace-pulse 中显示窗口提醒,以迭代结束作为自然发布窗口。 + +## 使用场景 + +### 场景 1:标准发布 + +``` +You: /pace-release +Claude: Found 3 merged CRs ready for release. + CR-001 (feature), CR-003 (defect), CR-005 (hotfix). + Create a new release? [Y/n] + +You: Y +Claude: Suggested version: 1.3.0 (contains features). + Confirm version? [1.3.0] + +You: 1.3.0 +Claude: REL-002 created (v1.3.0, staging). + Gate 4: Build OK, CI green, all CRs passed. + Next: /pace-release deploy + +You: /pace-release deploy +Claude: Deploy REL-002 (v1.3.0) to production? [Y/n] + +You: Y +Claude: Deployment recorded. Status: deployed. + Next: /pace-release verify + +You: /pace-release verify +Claude: Verification checklist (3/3 passed). + All items confirmed. Status: verified. + Next: /pace-release close + +You: /pace-release close +Claude: Generating changelog... done. + Version bump 1.2.0 -> 1.3.0? [Y] ... done. + Create tag v1.3.0? [Y] ... done. + + Release REL-002 complete (v1.3.0). + - Changelog written to CHANGELOG.md + - Version updated in package.json + - Tag v1.3.0 created + - 3 CRs marked as released + - Metrics updated (deploy frequency, lead time) +``` + +### 场景 2:部署后回滚 + +``` +You: /pace-release +Claude: REL-002 (v1.3.0) is deployed. Start verification? [Y/n/problem] + +You: problem +Claude: What happened? + +You: Payment processing is failing for international orders +Claude: Recording rollback for REL-002. + Reason: Payment processing failure (international orders). + Status: rolled_back. + + Create a hotfix CR for this issue? [Y/n] + +You: Y +Claude: CR-006 created (hotfix, linked to REL-002). + After fixing, create a new release with /pace-release create. +``` + +## 与其他命令的集成 + +| 命令 | 集成点 | +|------|--------| +| `/pace-dev` | CR 通过开发流水线达到 `merged` 状态,成为发布候选项 | +| `/pace-test` | `create` 通过 `/pace-test report` 自动生成 Release 级测试报告 | +| `/pace-feedback` | `verify` 中的问题触发通过反馈流创建 defect CR | +| `/pace-retro` | `close` 更新 dashboard.md 的 DORA 指标(部署频率、前置时间、变更失败率) | +| `/pace-sync` | 未来集成(Phase 19),用于外部平台发布状态同步 | + +## 相关资源 + +- [SKILL.md](../../skills/pace-release/SKILL.md) -- Skill 入口点和路由表 +- [release-procedures-common.md](../../skills/pace-release/release-procedures-common.md) -- 共享规则(版本推断 SSOT、发布规则、集成规则) +- [release-procedures-wizard.md](../../skills/pace-release/release-procedures-wizard.md) -- 引导向导(无参数流程) +- [release-procedures-create.md](../../skills/pace-release/release-procedures-create.md) -- 创建流程(CR 收集、版本建议) +- [release-procedures-create-enhanced.md](../../skills/pace-release/release-procedures-create-enhanced.md) -- 创建增强(依赖检测、就绪检查、Gate 4) +- [release-procedures-deploy.md](../../skills/pace-release/release-procedures-deploy.md) -- 部署流程(环境晋升) +- [release-procedures-verify.md](../../skills/pace-release/release-procedures-verify.md) -- 验证流程(健康检查) +- [release-procedures-close.md](../../skills/pace-release/release-procedures-close.md) -- 关闭/完成流程(8 步链) +- [release-procedures-changelog.md](../../skills/pace-release/release-procedures-changelog.md) -- Changelog 生成 +- [release-procedures-version.md](../../skills/pace-release/release-procedures-version.md) -- 版本号升级 +- [release-procedures-tag.md](../../skills/pace-release/release-procedures-tag.md) -- Git 标签和 GitHub Release +- [release-procedures-rollback.md](../../skills/pace-release/release-procedures-rollback.md) -- 回滚流程(候选项预填充) +- [release-procedures-notes.md](../../skills/pace-release/release-procedures-notes.md) -- Release Notes(角色视角) +- [release-procedures-branch.md](../../skills/pace-release/release-procedures-branch.md) -- 分支管理 +- [release-procedures-scheduling.md](../../skills/pace-release/release-procedures-scheduling.md) -- 发布调度 +- [release-procedures-status.md](../../skills/pace-release/release-procedures-status.md) -- 状态和历史 +- [integrations-format.md](../../knowledge/_schema/integrations-format.md) -- 集成配置 Schema +- [devpace-rules.md](../../rules/devpace-rules.md) -- 运行时行为规则 diff --git a/docs/features/pace-review_zh.md b/docs/features/pace-review_zh.md new file mode 100644 index 0000000..477f63c --- /dev/null +++ b/docs/features/pace-review_zh.md @@ -0,0 +1,206 @@ +# 代码审查与质量门禁(`/pace-review`) + +`/pace-review` 为处于 `in_review` 状态的变更请求(CR)生成结构化审查摘要。它将自动质量门禁检查(Gate 2)与对抗审查层和累积 Diff 报告相结合,然后移交给人类进行最终审批(Gate 3)。目标是以最低的认知负担,为审查者提供做出知情批准/拒绝决策所需的全部信息。 + +核心能力:复杂度自适应摘要深度(S 微型 / M 标准 / L-XL 含 TL;DR 的完整摘要)、reject-fix 周期的增量重审(Delta Review)、跨 CR 冲突检测、accept 报告引导的对抗聚焦、审查历史持久化,以及审批过程中的探索模式。 + +## 快速开始 + +``` +1. CR 到达 in_review 状态(通过 /pace-dev) +2. /pace-review → Gate 2 + 对抗审查 + 生成摘要 +3. 人类审查摘要 → "approved" / 拒绝 / 具体反馈 +4. approved → git merge → CR 转换为 merged +``` + +Gate 3(人类审批)在任何情况下都不可绕过(Iron Law IR-2)。 + +## 审查流程 + +### Step 1: 识别待审 CR + +扫描 `.devpace/backlog/` 中处于 `in_review` 状态的 CR。可选的关键词参数用于缩小到特定 CR。如果没有符合条件的 CR,**状态感知引导**会介入:建议检查 `verifying` 状态的 CR 或推进 `developing` 状态的 CR——用户无需了解状态机知识。 + +### Step 1.5: 跨 CR 冲突检测 + +扫描所有活跃 CR(developing/verifying/in_review),检测与当前 CR 在文件和模块层面的重叠。仅在检测到重叠时显示——无冲突时零噪音。 + +### Step 2: Gate 2——自动质量检查 + +Gate 2 是人类审查前的最后一道自动门禁。它遵循**独立验证原则**:不信任 Gate 1 的任何结果——所有证据重新采集(重新读取验收标准、重新获取 git diff)。 + +**执行顺序(强制)**: + +1. 从 CR 文件中重新读取验收标准 +2. 获取最新的 `git diff main...` +3. 首先检查意图一致性——如果意图不匹配,Gate 2 立即失败(不再执行后续检查) + +**意图一致性检查**将每条验收标准标记为:满足(pass)、未满足(fail + 缺少什么)、或部分满足(已完成与待完成的详细说明)。同时检测**范围外变更**和**范围内遗漏**。 + +**失败时**:CR 返回 `developing` 状态。Claude 修复差距、重新运行检查并重新提交——无需手动重启。 + +### Step 3: 对抗审查 + +Gate 2 通过后,思维模式从"验证正确性"切换为"发现缺陷"——这是对确认偏误的反制措施。 + +**核心规则**:零发现不可接受。如果所有维度都没有发现问题,至少输出一条可选优化建议作为下限。 + +**四个强制维度**(每个至少考虑一次): + +| 维度 | 示例 | +|------|------| +| 边界与错误路径 | 空输入、极端值、并发、超时 | +| 安全风险 | 注入、权限提升、敏感数据泄露 | +| 性能隐患 | N+1 查询、大量内存分配、阻塞操作 | +| 集成风险 | API 契约变更、向后兼容性 | + +严重性标签:`🔴 建议修复` / `🟡 建议改进` / `🟢 可选优化`。每条发现都附带误报免责声明。 + +**Accept 报告集成**:当存在 `/pace-test accept` 报告时,覆盖薄弱和低置信度区域会引导对抗聚焦——在易受攻击的代码路径上检查更多维度。 + +**可配置维度**:项目可在 `checks.md` 中定义自定义对抗维度(如数据一致性、无障碍访问、向后兼容性)。未配置时使用默认的 4 个维度。 + +**关键规则**:对抗发现不会阻断 Gate 2——它们是提供给人类审查者的参考信息。简单 CR(复杂度 S)完全跳过对抗审查。 + +### Step 4: 累积 Diff 报告 + +对于中等及以上复杂度的 CR,生成按模块分组的 diff 报告,将每个变更文件映射到对应的验收标准: + +``` +累积 diff 报告: + 模块 A (+N/-M 行): + - file1.ts (新增) → 验收标准 1 + - file2.ts (修改) → 验收标准 2 + ⚠️ 未覆盖的标准:[列表] + ⚠️ 范围外变更:[文件 + 理由] +``` + +这与 Gate 2 互补:Gate 2 检查"是否完成?",diff 报告展示"如何完成?"。简单 CR 跳过此报告。 + +### Step 5: 审查摘要 + +摘要深度根据 CR 复杂度自适应调整(遵循 P2 渐进暴露 + P6 三层透明原则): + +| 复杂度 | 摘要级别 | 内容 | +|--------|---------|------| +| S | 微型(3-5 行) | 变更内容 + 质量状态 + 等待审批 | +| M | 标准 | 意图匹配(含推理后缀)+ 对抗审查 + 累积 diff | +| L/XL | 完整 + TL;DR | 前置 2-3 行执行摘要,后接完整详情 | + +每条意图匹配判定附带不超过 15 字符的证据后缀(如 `RetryPolicy.ts:23 exponentialBackoff`)。用户可以追问"为什么?"来展开完整推理链。 + +**业务可追溯性**:自动追溯 CR → PF → BR 价值链。对不完整的追溯链如实标注,而非编造链接。 + +**审查历史**:摘要持久化到 CR 的验证证据章节(`### Review Summary (Round N, YYYY-MM-DD)`),支持会话恢复和增量重审基线。 + +详细规则见审查流程文件:[common](../../skills/pace-review/review-procedures-common.md)(始终加载)、[gate](../../skills/pace-review/review-procedures-gate.md)(M+ 审查)、[delta](../../skills/pace-review/review-procedures-delta.md)(增量审查)、[feedback](../../skills/pace-review/review-procedures-feedback.md)(决策后处理)。 + +### Step 6: 人类决定(Gate 3) + +| 人类响应 | 动作 | +|---------|------| +| "approved" / "lgtm" | CR → `approved` → `git merge` → CR → `merged` → 级联更新 | +| 拒绝 + 原因 | CR → `developing`;原因记录在事件表中(scope / quality / design) | +| 具体反馈 | Claude 修改代码 → 重新运行受影响的检查 → 更新摘要 | +| 探索性问题 | 暂停审批,进入探索模式(CR 保持 `in_review`),做出决定后恢复 | + +## 核心特性 + +### 反表演性意见处理 + +收到审查反馈时,Claude 遵循:**理解**真实意图(澄清歧义)→ **评估**与 CR 范围的对齐度 → **执行 + 验证**。禁止行为:回复"您说得对!"、接受违反 YAGNI 原则的建议、在理解意图之前修改代码。 + +### 独立验证原则 + +Gate 2 从零开始采集所有证据。不信任 Gate 1 的快照——上下文窗口会漂移,且代码可能在 `verifying` 阶段发生变更。这是一条硬性要求。 + +### 增量重审(Delta Review) + +当 CR 存在近期的 reject-fix 历史时,`/pace-review` 仅针对被拒绝时涉及的维度进行检查,而非重新运行完整审查。未变更的验收标准标记为"与上一轮相同"。用户可通过"完整审查"或"重新审查全部"强制执行完整审查。 + +### 简化审批集成 + +满足所有快速通道条件的简单 CR(S 复杂度、Gate 1/2 一次通过、0% 偏移)由 `/pace-dev` 内联审批处理。当用户在简化审批中选择"让我看看"时,`/pace-review` 以 S 级微型摘要激活——对于已通过验证的 CR 无需完整审查。 + +### 结构化拒绝记录 + +当人类拒绝 CR 时,Claude 提示输入分类原因(最多一次追问)。类别:**scope**(验收差距或范围蔓延)、**quality**(代码或测试覆盖率)、**design**(架构或 API)。记录存储在 CR 事件表中,供 `/pace-learn` 提取模式。 + +## 使用场景 + +### 场景 1:标准审查流程 + +``` +用户: /pace-review +Claude:找到 CR-007(in_review) + + ## 为支付网关添加重试逻辑 + **变更内容**:src/payment/ 下 3 个文件,新增 RetryPolicy 类 + **意图匹配**: + ✅ AC-1:指数退避 — 满足 + ✅ AC-2:最多重试 3 次 — 满足 + **对抗审查**(2 条发现): + 🟡 maxDelay 缺少上限校验 + 🟢 建议添加重试指标日志 + **质量**:✅ lint、types、tests | ⏳ 等待人类审批 + +用户: approved +Claude:✅ CR-007 → approved → 已合并到 main +``` + +### 场景 2:审查中的反馈修改 + +``` +用户: /pace-review CR-012 +Claude:[生成摘要] + +用户: parser.ts 的错误处理太宽泛了——请使用具体的 catch 块。 + +Claude:理解:按错误类型使用细粒度 catch 块。与 AC-2 对齐。 + 修改 parser.ts ... 重新运行检查 ... ✅ 全部通过。 + 更新摘要:✅ 所有标准满足 | ⏳ 等待人类审批 + +用户: lgtm +Claude:✅ CR-012 → approved → 已合并到 main +``` + +### 场景 3:拒绝后的增量重审 + +``` +用户: 错误处理太宽泛了——请使用具体的 catch 块。 +Claude:理解。CR-012 → developing。原因:quality: 宽泛的错误处理。 + + ...(修复完成,Gate 1/2 重新运行)... + +用户: /pace-review +Claude:找到 CR-012(in_review)—— 检测到之前的拒绝记录,执行增量重审。 + + ## 改进 parser 的错误处理(Delta Review · 第 2 轮) + **上次拒绝原因**:quality: 宽泛的错误处理 + **修复状态**: + ✅ 针对 ParseError、ValidationError、IOError 使用具体 catch 块 + **未变更**:AC-1(通过)、AC-3(通过)—— 未重新检查 + **质量**:✅ 全部通过 | ⏳ 等待人类审批 + +用户: lgtm +Claude:✅ CR-012 → approved → 已合并到 main +``` + +## 与其他 Skill 的集成 + +| Skill | 关系 | +|-------|------| +| `/pace-dev` | Gate 1 通过后将 CR 转换为 `in_review` 状态,移交给 `/pace-review` | +| `/pace-test` | 提供 `accept` 验证证据,在审查摘要中展示 | +| `/pace-change` | CR 状态转换(拒绝 → `developing`)遵循状态机 | +| `/pace-learn` | 结构化拒绝记录为模式提取提供数据,用于未来改进 | + +## 相关资源 + +- [SKILL.md](../../skills/pace-review/SKILL.md) -- Skill 入口点和触发描述 +- [review-procedures-common.md](../../skills/pace-review/review-procedures-common.md) -- 通用审查规则(始终加载) +- [review-procedures-gate.md](../../skills/pace-review/review-procedures-gate.md) -- M+ 审查流水线(意图、对抗、diff) +- [review-procedures-delta.md](../../skills/pace-review/review-procedures-delta.md) -- 增量审查流程 +- [review-procedures-feedback.md](../../skills/pace-review/review-procedures-feedback.md) -- 决策后处理 +- [设计文档](../design/design.md) -- 质量门禁定义和 CR 状态机 +- [devpace-rules.md](../../rules/devpace-rules.md) -- 运行时行为规则 diff --git a/docs/features/pace-test.md b/docs/features/pace-test.md index 0edd155..01e4e2e 100644 --- a/docs/features/pace-test.md +++ b/docs/features/pace-test.md @@ -184,7 +184,7 @@ AI-powered acceptance verification against PF criteria. **Syntax**: `/pace-test accept [CR-ID]` -The core differentiator of devpace testing. For each PF acceptance criterion, Claude selects a verification level -- L1 dynamic (execute tests/CLI), L2 static semantic (read code with line references), L3 manual (generate human checklists) -- and produces per-criterion evidence. Also performs a Test Oracle Check: reviews whether existing tests actually verify what they claim, downgrading weak or false coverage in `test-strategy.md`. See [verify-procedures.md](../../skills/pace-test/verify-procedures.md) for steps. +The core differentiator of devpace testing. For each PF acceptance criterion, Claude selects a verification level -- L1 dynamic (execute tests/CLI), L2 static semantic (read code with line references), L3 manual (generate human checklists) -- and produces per-criterion evidence. Also performs a Test Oracle Check: reviews whether existing tests actually verify what they claim, downgrading weak or false coverage in `test-strategy.md`. See [test-procedures-verify.md](../../skills/pace-test/test-procedures-verify.md) for steps. **Output example**: ``` @@ -255,6 +255,6 @@ Traditional tools measure **code coverage** ("what percentage of lines are execu - [test-procedures-coverage.md](../../skills/pace-test/test-procedures-coverage.md) -- Coverage - [test-procedures-impact.md](../../skills/pace-test/test-procedures-impact.md) -- Impact - [test-procedures-report.md](../../skills/pace-test/test-procedures-report.md) -- Reports -- [verify-procedures.md](../../skills/pace-test/verify-procedures.md) -- Acceptance verification +- [test-procedures-verify.md](../../skills/pace-test/test-procedures-verify.md) -- Acceptance verification - [test-procedures-advanced.md](../../skills/pace-test/test-procedures-advanced.md) -- Flaky, dryrun, baseline - [devpace-rules.md](../../rules/devpace-rules.md) -- Runtime behavior rules diff --git a/docs/features/pace-test_zh.md b/docs/features/pace-test_zh.md new file mode 100644 index 0000000..a874109 --- /dev/null +++ b/docs/features/pace-test_zh.md @@ -0,0 +1,261 @@ +# 测试策略与质量管理(`/pace-test`) + +devpace 的测试远不止"跑测试、看结果"。`/pace-test` 管理的是一条**需求驱动**的测试生命周期:Product Feature (PF) 的验收标准定义了需要验证什么,测试策略将标准映射到测试类型,覆盖分析衡量的是需求覆盖率(而非仅仅是代码覆盖率),AI 驱动的验收验证则为每条标准提供带代码级引用的证据。最终结果是一套每一步都可追溯到业务意图的测试流程。 + +## 前置条件 + +| 条件 | 用途 | 是否必须 | +|------|------|:--------:| +| `.devpace/` 已初始化 | 项目结构、PF 定义、CR 追踪 | 是 | +| `.devpace/rules/checks.md` | 为 `run` 和 `dryrun` 配置测试命令 | 推荐 | +| `.devpace/rules/test-strategy.md` | PF 到测试的映射(由 `strategy` 生成) | 推荐 | + +> **优雅降级**:若未初始化 `.devpace/`,Skill 回退到纯代码模式(自动检测测试命令)。若缺少 `checks.md`,会从 `package.json`、`pyproject.toml`、`go.mod` 或 `Cargo.toml` 中自动检测常见测试命令。若缺少 `test-strategy.md`,需求级分析仍可通过直接读取 `project.md` 中的 PF 标准运作。 + +## 快速开始 + +``` +1. /pace-test strategy --> 将 PF 验收标准映射到测试类型 +2. /pace-test generate PF-001 --full --> 基于标准生成测试用例 +3. /pace-test coverage --> 分析需求覆盖缺口 +4. /pace-test --> 运行所有已配置的测试 +5. /pace-test accept --> AI 验收验证 +6. /pace-test report --> 生成可供评审的摘要报告 +``` + +日常工作流:大多数会话只需 `/pace-test`(run)+ `/pace-test accept`。迭代期间使用 `impact --run` 可快速执行受影响的测试。 + +## 命令参考 + +### 执行层(Execution Layer) + +#### `run`(默认,无参数) + +执行所有已配置的测试命令并生成结构化报告。 + +**语法**:`/pace-test` + +读取 `checks.md` 中的测试命令(回退到自动检测),按依赖顺序执行,生成按 Gate 分组的通过/失败报告。当测试失败时,分析最近的 `git diff` 推断可能的根因。配置了 `.devpace/integrations/config.md` 时会自动附加 CI 运行状态。详细步骤见 [test-procedures-core.md](../../skills/pace-test/test-procedures-core.md)。 + +**输出示例**: +``` +| # | Check | Gate | Status | Time | Notes | +|---|----------|--------|--------|------|---------------------| +| 1 | npm test | Gate 1 | PASS | 3.2s | -- | +| 2 | eslint . | Gate 1 | FAIL | 1.1s | 3 errors in auth.ts | +Summary: 1/2 passed +Suggestion: run /pace-test dryrun 1 to pre-check Gate 1 +``` + +#### `generate` + +基于 PF 验收标准创建测试用例。 + +**语法**:`/pace-test generate [PF-title] [--full]` + +默认(skeleton)模式生成带 `// TODO` 占位符的脚手架代码。`--full` 模式生成包含断言、边界条件和错误路径的完整实现(标记 `// REVIEW: AI-generated`)。自动注册到 `test-strategy.md`。在 TDD 上下文中会追加 Red-Green-Refactor 提醒。详细步骤见 [test-procedures-generate.md](../../skills/pace-test/test-procedures-generate.md)。 + +**输出示例**: +``` +Generated 4 test cases [full] for PF "User Authentication" (4 criteria): +1. Users can log in --> test_login_email_password [3 assertions + 2 boundary] +2. Failed login error --> test_login_failure_message [2 assertions] +File: tests/test_auth.py | Mode: full (review REVIEW markers) +``` + +#### `dryrun` + +模拟 Gate 检查,不触发状态转换。 + +**语法**:`/pace-test dryrun [1|2|4]` + +以只读模式执行完整的 Gate 检查流程(命令检查、意图检查、Gate 2 的对抗性审查)。不产生 CR 状态变更,不写入事件日志。详细步骤见 [test-procedures-advanced.md](../../skills/pace-test/test-procedures-advanced.md)。 + +**输出示例**: +``` +Gate 1 Dry-Run: 1 PASS / 1 FAIL +Prediction: Gate will FAIL +Fix: resolve eslint errors, then re-run /pace-test dryrun 1 +``` + +### 策略层(Strategy Layer) + +#### `strategy` + +基于 PF 验收标准生成系统化测试策略。 + +**语法**:`/pace-test strategy` + +针对每条验收标准,推荐一种主要测试类型(unit、integration、E2E、performance、security、accessibility、manual)和 0-2 种辅助类型。通过名称和内容分析匹配现有测试文件。输出测试金字塔健康评估和实施指引。持久化到 `.devpace/rules/test-strategy.md`。详细步骤见 [test-procedures-strategy-gen.md](../../skills/pace-test/test-procedures-strategy-gen.md)。 + +**输出示例**: +``` +Strategy: 3 PFs, 12 criteria --> 5 unit / 3 integration / 2 E2E / 1 perf [+security] / 1 manual +Covered: 7 | To build: 5 +Pyramid health: needs attention (unit 42%, below 50% threshold) +Next: /pace-test generate [PF] to create tests for uncovered criteria +``` + +#### `coverage` + +分析有多少 PF 验收标准已有对应测试。 + +**语法**:`/pace-test coverage` + +交叉比对 PF 标准与 `test-strategy.md`、`checks.md` 及扫描到的测试文件。可选地收集代码覆盖率作为补充信号(Jest、pytest、go test、cargo tarpaulin)。当 `test-strategy.md` 包含阈值配置时,检查数值是否达标。详细步骤见 [test-procedures-coverage.md](../../skills/pace-test/test-procedures-coverage.md)。 + +**输出示例**: +``` +| PF | Feature | Criteria | Covered | Rate | +|--------|----------------|----------|---------|------| +| PF-001 | Authentication | 5 | 3 | 60% | +| PF-002 | Data Import | 3 | 0 | 0% | +Requirements coverage: 3/8 (38%) +Code coverage (supplementary): 72% line, 58% branch (Jest) +``` + +#### `impact` + +分析变更影响并推荐回归测试范围。 + +**语法**:`/pace-test impact [CR-ID] [--run]` + +从 `git diff` 提取变更文件,构建文件到 PF 的反向映射,识别直接和间接受影响的 PF,并评定风险等级。使用 `--run` 时,在分析后自动执行"必须运行"的测试。详细步骤见 [test-procedures-impact.md](../../skills/pace-test/test-procedures-impact.md)。 + +**输出示例**: +``` +CR-005 "Add CSV export" | Scope: 6 files / 2 modules | Risk: MEDIUM +| PF | Feature | Impact | Suggested Tests | +|--------|-------------|---------|------------------------| +| PF-002 | Data Import | Direct | test_import, test_csv | +| PF-003 | Reports | Indirect| Spot-check recommended | +Must-run: test_import, test_csv +``` + +### 分析层(Analysis Layer) + +#### `report` + +生成面向评审或发布的可读测试摘要。 + +**语法**:`/pace-test report [CR-ID|REL-xxx]` + +**CR 模式**(默认):聚合 Layer 1(测试执行)、Layer 2(需求覆盖)、Layer 3(AI 验收验证),生成合并/拒绝建议。**Release 模式**(`REL-xxx`):聚合发布内所有 CR,提供逐 CR 质量摘要和发布/延期建议。遵循"有什么报什么"原则——缺失的层级会注明,但不阻断报告生成。详细步骤见 [test-procedures-report.md](../../skills/pace-test/test-procedures-report.md)。 + +**输出示例**(CR 模式): +``` +CR-005 | L1: 8/8 passed | L2: 3/5 covered (60%) | L3: 4/5 passed, 1 manual +Recommendation: supplement tests before merge +``` + +#### `flaky` + +检测不稳定测试和主动维护问题。 + +**语法**:`/pace-test flaky` + +扫描 CR 事件历史中的间歇性故障、环境依赖故障、顺序依赖故障和超时波动。执行主动维护检测:空断言、时间膨胀、长期未更新的测试和被跳过的测试。持久化到 `insights.md`,并在 `test-strategy.md` 中降级不稳定测试。详细步骤见 [test-procedures-advanced.md](../../skills/pace-test/test-procedures-advanced.md)。 + +**输出示例**: +``` +Unstable: e2e-login 2/5 (40%) intermittent [CR-003, CR-005] +Maintenance: test_utils::helper (empty assert) | lint (+217% bloat) +Priority: fix empty assertions first (false security) +``` + +#### `baseline` + +建立或更新测试执行基线,用于趋势追踪。 + +**语法**:`/pace-test baseline` + +运行完整测试套件,记录通过率和执行时间,与上一次基线对比。持久化到 `.devpace/rules/test-baseline.md`。由 `/pace-retro` 消费以用于度量分析。详细步骤见 [test-procedures-advanced.md](../../skills/pace-test/test-procedures-advanced.md)。 + +**输出示例**: +``` +Baseline updated: pass rate 85%->92% (+7%) | exec 12.3s->10.1s (-2.2s) | checks 8->10 (+2) +``` + +### 验证层(Verification Layer) + +#### `accept` + +基于 PF 标准的 AI 驱动验收验证。 + +**语法**:`/pace-test accept [CR-ID]` + +这是 devpace 测试的核心差异化能力。针对每条 PF 验收标准,Claude 选择一个验证级别——L1 动态验证(执行测试/CLI)、L2 静态语义验证(读取代码并提供行号引用)、L3 手动验证(生成人工检查清单)——并为每条标准生成证据。同时执行 Test Oracle Check:审查现有测试是否真正验证了其声称的内容,并在 `test-strategy.md` 中降级弱覆盖或虚假覆盖。详细步骤见 [test-procedures-verify.md](../../skills/pace-test/test-procedures-verify.md)。 + +**输出示例**: +``` +CR-005 "Add CSV export" (PF: Data Import) +| # | Criterion | Status | Level | Evidence | +|---|----------------------------|---------|--------|---------------------------------| +| A1| CSV parses all columns | PASS | L1 | test_csv_parser passed | +| A2| Error on malformed rows | PASS | L2 | src/parser.ts:45 validates rows | +| A3| Progress bar during upload | PARTIAL | L2 | Complex async, needs runtime | +| A4| Accessibility | MANUAL | L3 | Checklist generated | +Summary: 2 passed, 1 needs supplement, 1 needs manual check +``` + +**降级行为**:若无 PF 关联,回退到 CR 意图验收标准(在报告中注明)。若两者均缺失,退出并给出明确提示。 + +## 核心差异化:需求驱动的测试 + +传统工具衡量的是**代码覆盖率**("多少百分比的代码行被执行了")。devpace 衡量的是**需求覆盖率**("多少百分比的 PF 验收标准有对应验证")。一个项目可以拥有 95% 的代码覆盖率,却有 0% 的需求覆盖率。`/pace-test` 通过 strategy-generate-coverage-accept-report 管道弥合这一缺口——每一个测试都可追溯到一条 PF 验收标准。 + +## 使用场景 + +### 场景 1:新功能测试规划(TDD) + +``` +/pace-test strategy --> PF-003: 6 criteria, 3 unit / 2 integration / 1 E2E +/pace-test generate PF-003 --full --> 6 test cases. TDD: run to confirm Red phase. +/pace-test coverage --> PF-003 requirements: 6/6 (100%). Code: 0% (expected). +``` + +### 场景 2:合并前质量检查 + +``` +/pace-test accept CR-005 --> 4/5 passed, 1 manual. Oracle: weak coverage found. +/pace-test report CR-005 --> 3-layer report. Recommendation: supplement, then merge. +``` + +### 场景 3:发布就绪评估 + +``` +/pace-test report REL-001 --> 5 CRs, 3 PFs. Risk: LOW. Can ship. +``` + +## 与其他命令的集成 + +| 命令 | 集成点 | +|------|--------| +| `/pace-dev` | Gate 1/2 消费 `checks.md`(与 `run` 执行相同命令)。`accept` 报告作为 Gate 2 证据。 | +| `/pace-review` | Gate 2 消费 `accept` 验证报告作为结构化评审证据。 | +| `/pace-release` | `report REL-xxx` 生成发布级质量报告。`dryrun 4` 验证发布前检查。 | +| `/pace-retro` | `baseline` 为回顾提供度量数据。`flaky` 发现写入 `insights.md`。 | +| `/pace-change` | `impact` 使用 CR 变更范围确定回归测试建议。 | + +## 向后兼容 + +| 旧名称 | 当前名称 | 说明 | +|--------|---------|------| +| `verify` | `accept` | AI 验收验证 | +| `regress` | `impact` | 变更影响分析 | +| `gen` | `generate` | 测试用例生成 | +| `gate` | `dryrun` | Gate 模拟 | + +## 相关资源 + +- [User Guide -- /pace-test 章节](../user-guide.md) +- [SKILL.md](../../skills/pace-test/SKILL.md) -- 入口与路由表 +- [test-procedures-core.md](../../skills/pace-test/test-procedures-core.md) -- Run、CI 集成 +- [test-procedures-strategy-gen.md](../../skills/pace-test/test-procedures-strategy-gen.md) -- Strategy +- [test-procedures-coverage.md](../../skills/pace-test/test-procedures-coverage.md) -- Coverage +- [test-procedures-impact.md](../../skills/pace-test/test-procedures-impact.md) -- Impact +- [test-procedures-report.md](../../skills/pace-test/test-procedures-report.md) -- Reports +- [test-procedures-verify.md](../../skills/pace-test/test-procedures-verify.md) -- 验收验证 +- [test-procedures-advanced.md](../../skills/pace-test/test-procedures-advanced.md) -- Flaky、dryrun、baseline +- [test-procedures-generate.md](../../skills/pace-test/test-procedures-generate.md) -- Generate +- [devpace-rules.md](../../rules/devpace-rules.md) -- 运行时行为规则 diff --git a/promt.md b/docs/scratch/prompt-notes.md similarity index 100% rename from promt.md rename to docs/scratch/prompt-notes.md diff --git a/knowledge/_schema/accept-report-contract.md b/knowledge/_schema/accept-report-contract.md index 10130c4..e1bb8f8 100644 --- a/knowledge/_schema/accept-report-contract.md +++ b/knowledge/_schema/accept-report-contract.md @@ -2,13 +2,13 @@ > **职责**:定义 `/pace-test accept` 验收验证报告的输出格式契约。此文件是 pace-test(生产方)和 pace-review(消费方)之间的共享接口。 > -> **修改此文件时**:必须同时检查生产方(`verify-procedures.md` Step 4)和消费方(`review-procedures-gate.md` accept 消费章节)是否需要适配。 +> **修改此文件时**:必须同时检查生产方(`test-procedures-verify.md` Step 4)和消费方(`review-procedures-gate.md` accept 消费章节)是否需要适配。 ## §0 速查卡片 | 属性 | 值 | |------|-----| -| 生产方 | `/pace-test accept`(verify-procedures.md Step 4) | +| 生产方 | `/pace-test accept`(test-procedures-verify.md Step 4) | | 消费方 | `/pace-review`(review-procedures-gate.md accept 消费章节) | | 写入位置 | CR 文件"验证证据" section | | 触发标题 | `## 验收验证报告` | diff --git a/scripts/validate-all.sh b/scripts/validate-all.sh index 8a44808..9887924 100755 --- a/scripts/validate-all.sh +++ b/scripts/validate-all.sh @@ -90,6 +90,40 @@ echo -e " ℹ Product layer total: ${TOTAL_PRODUCT} lines" echo "" +# ── Tier 1.9: Hook tests (Node.js) ───────────────────────────────────── +echo -e "${YELLOW}[Tier 1.9] Hook tests (Node.js)${NC}" + +HOOKS_TEST_DIR="$PROJECT_ROOT/tests/hooks" +if [ -d "$HOOKS_TEST_DIR" ]; then + if command -v node &>/dev/null; then + HOOK_PASS=0 + HOOK_FAIL=0 + for test_file in "$HOOKS_TEST_DIR"/test_*.mjs; do + [ -f "$test_file" ] || continue + if node --test "$test_file" >/dev/null 2>&1; then + HOOK_PASS=$((HOOK_PASS + 1)) + else + HOOK_FAIL=$((HOOK_FAIL + 1)) + echo -e "${RED} ✗ $(basename "$test_file")${NC}" + fi + done + if [ "$HOOK_FAIL" -eq 0 ] && [ "$HOOK_PASS" -gt 0 ]; then + echo -e "${GREEN} ✓ Hook tests passed (${HOOK_PASS}/${HOOK_PASS})${NC}" + elif [ "$HOOK_PASS" -eq 0 ] && [ "$HOOK_FAIL" -eq 0 ]; then + echo -e "${YELLOW} ⚠ No hook test files found${NC}" + else + echo -e "${RED} ✗ Hook tests: ${HOOK_PASS} passed, ${HOOK_FAIL} failed${NC}" + FAILURES=$((FAILURES + 1)) + fi + else + echo -e "${YELLOW} ⚠ node not found — skipping hook tests${NC}" + fi +else + echo -e "${YELLOW} ⚠ tests/hooks/ directory not found${NC}" +fi + +echo "" + # ── Tier 2: Integration test (optional — requires claude CLI) ────────── echo -e "${YELLOW}[Tier 2] Integration test (plugin loading)${NC}" diff --git a/skills/pace-biz/SKILL.md b/skills/pace-biz/SKILL.md index 16ca23e..a24eea3 100644 --- a/skills/pace-biz/SKILL.md +++ b/skills/pace-biz/SKILL.md @@ -1,7 +1,8 @@ --- description: Use when user says "业务机会", "专题", "Epic", "分解需求", "战略对齐", "业务全景", "业务规划", "需求发现", "头脑风暴", "brainstorm", "导入需求", "从文档导入", "代码分析需求", "技术债务盘点", "discover", "import", "infer", "pace-biz", or wants to create opportunities/Epics, decompose requirements, discover/import/infer features. NOT for implementation (/pace-dev), existing item changes (/pace-change), or iteration planning (/pace-plan). -allowed-tools: AskUserQuestion, Write, Read, Edit, Glob, Bash, Grep +allowed-tools: AskUserQuestion, Read, Write, Edit, Glob, Grep, Bash argument-hint: "[opportunity|epic|decompose|align|view|discover|import|infer] [EPIC-xxx|BR-xxx] <描述|路径>" +model: sonnet context: fork agent: pace-pm --- diff --git a/skills/pace-change/SKILL.md b/skills/pace-change/SKILL.md index 29a4fc3..70fd300 100644 --- a/skills/pace-change/SKILL.md +++ b/skills/pace-change/SKILL.md @@ -1,7 +1,8 @@ --- description: Use when user says "不做了", "先不搞", "加一个", "加需求", "改一下", "改需求", "优先级调", "优先级调整", "延后", "提前", "砍掉", "插入", "新增需求", "先做这个", "恢复之前的", "恢复", "搁置", "放一放", "范围变了", "不要这个功能了", "追加", "补一个", "还需要", "改个需求", "需求变了", "停掉", "捡回来", "排到前面", "pace-change", or wants to add, pause, resume, reprioritize, modify, undo, batch change, or query change history. NOT for code implementation (use /pace-dev) or project initialization (use /pace-init). -allowed-tools: AskUserQuestion, Write, Read, Edit, Glob, Bash, Grep +allowed-tools: AskUserQuestion, Read, Write, Edit, Glob, Grep, Bash argument-hint: "[add|pause|resume|reprioritize|modify|batch|undo|history|apply] [#N|--last|--dry-run] <描述>" +model: sonnet context: fork agent: pace-pm --- diff --git a/skills/pace-dev/SKILL.md b/skills/pace-dev/SKILL.md index 866d247..a2f8f7b 100644 --- a/skills/pace-dev/SKILL.md +++ b/skills/pace-dev/SKILL.md @@ -1,7 +1,8 @@ --- description: Use when user says "开始做", "帮我改", "实现", "修复", "继续推进", "编码", "写代码", "开发", "重构", "做个", "coding", "implement", "fix", "refactor", "build", /pace-dev, or explicitly requests to start, continue, or resume coding/development work on a feature or bug fix. "帮我改" applies when the target is code, UI, or configuration — not requirements or acceptance criteria. NOT for requirement changes (use /pace-change) or code review (use /pace-review). NOT for running tests (use /pace-test). NOT for user-reported production issues (use /pace-feedback). -allowed-tools: AskUserQuestion, Write, Read, Edit, Glob, Bash +allowed-tools: AskUserQuestion, Read, Write, Edit, Glob, Bash argument-hint: "[<功能描述>|#|--last]" +model: sonnet context: fork agent: pace-engineer hooks: diff --git a/skills/pace-feedback/SKILL.md b/skills/pace-feedback/SKILL.md index b37c68b..91bb2c3 100644 --- a/skills/pace-feedback/SKILL.md +++ b/skills/pace-feedback/SKILL.md @@ -1,9 +1,11 @@ --- -description: Use when user reports issues, shares feedback, or receives production alerts — "用户反馈", "线上问题", "生产问题", "告警", "改进建议", "新需求", "体验问题", "功能请求", "线上bug", "运维", "事件", "incident", "故障", "P0", "P1", "严重故障", "postmortem", "事后复盘". -allowed-tools: AskUserQuestion, Write, Read, Edit, Glob, Bash +description: Use when user reports issues, shares feedback, or receives production alerts — "用户反馈", "线上问题", "生产问题", "告警", "改进建议", "新需求", "体验问题", "功能请求", "线上bug", "运维", "事件", "incident", "故障", "P0", "P1", "严重故障", "postmortem", "事后复盘". NOT for code implementation or development (use /pace-dev). NOT for requirement changes (use /pace-change). +allowed-tools: AskUserQuestion, Read, Write, Edit, Glob, Bash argument-hint: "[report <问题描述>] 或 [incident open/close/timeline/list] 或 [反馈描述]" model: sonnet disable-model-invocation: true +context: fork +agent: pace-engineer --- # /pace-feedback — 反馈收集与事件处理 diff --git a/skills/pace-guard/SKILL.md b/skills/pace-guard/SKILL.md index 98fe0c0..3302493 100644 --- a/skills/pace-guard/SKILL.md +++ b/skills/pace-guard/SKILL.md @@ -1,6 +1,6 @@ --- description: Use when user wants to assess risks before development, check current risk status, analyze risk trends, or says "风险/预检/预分析/guard/risk/隐患/安全检查". Also auto-invoked during advance mode intent checkpoint for L/XL CRs. NOT for /pace-dev (implementation), NOT for /pace-review (quality gate), NOT for /pace-test (testing). -allowed-tools: Read, Glob, Grep, Write, Edit, Bash +allowed-tools: Read, Write, Edit, Glob, Grep, Bash model: sonnet argument-hint: "[scan|monitor|trends|report|resolve] [CR编号] [--full|--brief|--detail|--batch]" context: fork diff --git a/skills/pace-init/SKILL.md b/skills/pace-init/SKILL.md index 2ba8134..98243cf 100644 --- a/skills/pace-init/SKILL.md +++ b/skills/pace-init/SKILL.md @@ -1,6 +1,6 @@ --- description: Use when user says "初始化", "pace-init", "开始追踪", "初始化研发管理", "新项目", "项目管理", "set up devpace", "健康检查 devpace", "重置 devpace", "预览初始化", or wants to set up, verify, or reset project development tracking. NOT for current progress overview (use /pace-status) or starting development (use /pace-dev). -allowed-tools: AskUserQuestion, Write, Read, Edit, Glob, Bash +allowed-tools: AskUserQuestion, Read, Write, Edit, Glob, Bash argument-hint: "[项目名称] [full] [--from <路径>...] [--import-insights <路径>] [--verify [--fix]] [--dry-run] [--reset [--keep-insights]] [--export-template] [--from-template <路径>] [--interactive] [--lite]" model: sonnet disable-model-invocation: true diff --git a/skills/pace-learn/SKILL.md b/skills/pace-learn/SKILL.md index 174ea67..2f03064 100644 --- a/skills/pace-learn/SKILL.md +++ b/skills/pace-learn/SKILL.md @@ -1,5 +1,5 @@ --- -description: Use when user says "/pace-learn" for knowledge base management, or auto-invoked after CR merge, gate failure recovery, or human rejection. +description: Use when user says "/pace-learn", "经验", "知识库", "pattern", "lessons learned", "学到了什么", or auto-invoked after CR merge, gate failure recovery, or human rejection. allowed-tools: Read, Write, Edit, Glob, Grep model: sonnet argument-hint: "[note|list|stats|export] [参数]" diff --git a/skills/pace-plan/SKILL.md b/skills/pace-plan/SKILL.md index 02d469c..dac64ce 100644 --- a/skills/pace-plan/SKILL.md +++ b/skills/pace-plan/SKILL.md @@ -1,7 +1,8 @@ --- description: Use when user says "规划迭代", "下个迭代做什么", "迭代规划", "计划", "排期", "安排", "sprint", "pace-plan", "调整迭代范围", "迭代调整", "迭代健康", or at iteration boundary when planning next iteration scope. NOT for PF-level requirement changes (use /pace-change). -allowed-tools: AskUserQuestion, Write, Read, Edit, Glob +allowed-tools: AskUserQuestion, Read, Write, Edit, Glob argument-hint: "[next|close|adjust|health]" +model: sonnet context: fork agent: pace-pm --- diff --git a/skills/pace-pulse/SKILL.md b/skills/pace-pulse/SKILL.md index 4bbcdd1..b073e0a 100644 --- a/skills/pace-pulse/SKILL.md +++ b/skills/pace-pulse/SKILL.md @@ -1,7 +1,7 @@ --- -description: Auto-invoked during advance mode after 5 checkpoints or 30+ minutes on same CR, at session start/end, or when rhythm anomalies are detected. +description: Auto-invoked during advance mode after extended work on same CR, at session start/end, or when rhythm anomalies are detected. user-invocable: false -allowed-tools: Read, Glob, Write +allowed-tools: Read, Write, Glob model: haiku --- diff --git a/skills/pace-release/SKILL.md b/skills/pace-release/SKILL.md index 1f5c9a6..1b92685 100644 --- a/skills/pace-release/SKILL.md +++ b/skills/pace-release/SKILL.md @@ -1,6 +1,6 @@ --- description: Use when user says "发布", "部署", "上线", "release", "pace-release", or wants to create, deploy, or close a release. -allowed-tools: AskUserQuestion, Write, Read, Edit, Glob, Bash +allowed-tools: AskUserQuestion, Read, Write, Edit, Glob, Bash argument-hint: "[create|deploy|verify|close|full|status|status history|changelog|version|tag|notes --role biz|ops|pm|branch|rollback]" model: sonnet disable-model-invocation: true diff --git a/skills/pace-retro/SKILL.md b/skills/pace-retro/SKILL.md index 8009c1d..2a84507 100644 --- a/skills/pace-retro/SKILL.md +++ b/skills/pace-retro/SKILL.md @@ -2,6 +2,7 @@ description: Use when user says "回顾", "复盘", "度量", "retro", "总结", "数据分析", "DORA", "质量报告", "交付效率", "度量报告", "趋势", "中期检查", "对比", "预测", "forecast", "能按时交付吗", "交付概率", "瓶颈", "pace-retro", or at iteration end when reviewing progress and metrics. allowed-tools: Read, Write, Edit, Glob, Bash argument-hint: "[update|focus <维度>|compare|history|mid|accept|forecast]" +model: sonnet context: fork agent: pace-analyst --- diff --git a/skills/pace-review/SKILL.md b/skills/pace-review/SKILL.md index 0500e9a..572d209 100644 --- a/skills/pace-review/SKILL.md +++ b/skills/pace-review/SKILL.md @@ -1,6 +1,6 @@ --- description: Use when user says "review", "审核", "帮我看看", "代码审查", "提交审核", "Gate 2", "提交审批", "pace-review", or when a change request reaches in_review state. NOT for running tests or acceptance verification (use /pace-test). -allowed-tools: Read, Write, Edit, Glob, Bash, AskUserQuestion +allowed-tools: AskUserQuestion, Read, Write, Edit, Glob, Bash argument-hint: "[<关键词>]" model: opus context: fork diff --git a/skills/pace-role/SKILL.md b/skills/pace-role/SKILL.md index e92402f..5ff634e 100644 --- a/skills/pace-role/SKILL.md +++ b/skills/pace-role/SKILL.md @@ -1,6 +1,6 @@ --- description: Use when user wants to switch output perspective (视角切换), says "切换角色/视角", "以XX视角", "pace-role", "作为产品经理", "作为运维", "换个角度看", or wants to view project from a different role perspective. -allowed-tools: Read, Glob, Write +allowed-tools: Read, Write, Glob argument-hint: "[biz 业务视角|pm 产品视角|dev 开发视角|tester 测试视角|ops 运维视角|auto 自动推断|compare 多视角快照]" model: haiku --- @@ -36,6 +36,8 @@ $ARGUMENTS: 角色关注维度权威定义及跨 Skill 适配原则见 `role-procedures-dimensions.md`。 +> **隐式依赖**:`role-procedures-inference.md` 不在路由表中直接路由,但由 `rules/devpace-rules.md` §10 在运行时加载(自动推断模式的关键词映射权威源)。 + ## 输出 - 角色切换:确认信息(1-3 行,含相关性评估摘要) diff --git a/skills/pace-sync/SKILL.md b/skills/pace-sync/SKILL.md index e52bb53..80d80e0 100644 --- a/skills/pace-sync/SKILL.md +++ b/skills/pace-sync/SKILL.md @@ -1,7 +1,7 @@ --- -description: "Use when user wants to sync devpace state with external tools (GitHub/Linear/Jira), says '同步/sync/push/pull/关联 Issue/配置同步/setup/解除关联/unlink/创建 Issue/create/同步状态/status/CI/构建/build/pipeline/workflow/GitHub Actions', or /pace-sync. NOT for internal devpace state changes (use /pace-dev) or release operations (use /pace-release)" +description: Use when user wants to sync devpace state with external tools (GitHub/Linear/Jira), says "同步", "sync", "push", "pull", "关联 Issue", "配置同步", "setup", "解除关联", "unlink", "创建 Issue", "create", "同步状态", "status", "CI", "构建", "build", "pipeline", "workflow", "GitHub Actions", or /pace-sync. NOT for internal devpace state changes (use /pace-dev) or release operations (use /pace-release). argument-hint: "[子命令] [参数]" -allowed-tools: Read, Write, Edit, Glob, Grep, Bash, AskUserQuestion +allowed-tools: AskUserQuestion, Read, Write, Edit, Glob, Grep, Bash model: sonnet --- diff --git a/skills/pace-test/SKILL.md b/skills/pace-test/SKILL.md index 74d028f..a1377b9 100644 --- a/skills/pace-test/SKILL.md +++ b/skills/pace-test/SKILL.md @@ -1,6 +1,6 @@ --- description: Use when user says "跑测试", "测试覆盖", "验证一下", "验收", "回归", "影响分析", "test", "verify", "accept", "coverage", "测试策略", /pace-test, or when test results, coverage gaps, or acceptance readiness are discussed. -allowed-tools: AskUserQuestion, Write, Read, Edit, Glob, Grep, Bash +allowed-tools: AskUserQuestion, Read, Write, Edit, Glob, Grep, Bash argument-hint: "[accept|strategy|coverage|impact|report|generate|...] [目标]" model: sonnet context: fork @@ -20,7 +20,7 @@ agent: pace-engineer ### accept 的定位 -Gate 2 仅二元判定整体一致性。accept 提供精细能力:逐条验收标准附证据、三级判定(✅/⚠️/❌)、测试预言审查断言实质性、弱覆盖自动降级策略。不做 accept 也能过 Gate 2,但做了的 CR 在 Gate 3 有更充分的证据支撑(详见 verify-procedures.md)。 +Gate 2 仅二元判定整体一致性。accept 提供精细能力:逐条验收标准附证据、三级判定(✅/⚠️/❌)、测试预言审查断言实质性、弱覆盖自动降级策略。不做 accept 也能过 Gate 2,但做了的 CR 在 Gate 3 有更充分的证据支撑(详见 test-procedures-verify.md)。 ## 输入 @@ -66,7 +66,7 @@ $ARGUMENTS: | 参数 | 流程 | 详细规程 | |------|------|---------| | (空) | Layer 1 基础执行 | `test-procedures-core.md` §1 | -| `accept`(旧名 `verify`) | Layer 3 AI 验收验证 | `verify-procedures.md` | +| `accept`(旧名 `verify`) | Layer 3 AI 验收验证 | `test-procedures-verify.md` | | `generate`(旧名 `gen`) | 测试用例生成 | `test-procedures-generate.md`(自包含) | | `strategy` | 测试策略生成 | `test-procedures-strategy-gen.md` | | `coverage` | 需求覆盖分析 | `test-procedures-coverage.md` | diff --git a/skills/pace-test/test-procedures-dryrun.md b/skills/pace-test/test-procedures-dryrun.md index 4d41355..570dd10 100644 --- a/skills/pace-test/test-procedures-dryrun.md +++ b/skills/pace-test/test-procedures-dryrun.md @@ -26,7 +26,7 @@ - 命令检查:实际执行 bash 命令,记录结果 - 意图检查:Claude 按规则判定,输出结论 - 对抗审查(Gate 2):执行对抗审查,输出发现 - - 浏览器验收(Gate 2,前端项目 + Playwright MCP 可用时):按 `verify-procedures.md` L1+ 流程执行,标注"🖥️ 浏览器验收" + - 浏览器验收(Gate 2,前端项目 + Playwright MCP 可用时):按 `test-procedures-verify.md` L1+ 流程执行,标注"🖥️ 浏览器验收" - **不触发 CR 状态转换**——仅输出结果 4. **生成模拟报告** diff --git a/skills/pace-test/verify-procedures.md b/skills/pace-test/test-procedures-verify.md similarity index 100% rename from skills/pace-test/verify-procedures.md rename to skills/pace-test/test-procedures-verify.md diff --git a/skills/pace-trace/SKILL.md b/skills/pace-trace/SKILL.md index 5713120..ecc560f 100644 --- a/skills/pace-trace/SKILL.md +++ b/skills/pace-trace/SKILL.md @@ -1,6 +1,6 @@ --- description: Use when user asks "why did devpace decide X", "追溯", "为什么这样做", "决策记录", "决策原因", "架构决策", "ADR", "技术选型", wants to see AI decision trail or manage architecture decisions, or says /pace-trace [CR] [gate/decision/arch] -allowed-tools: Read, Glob, Grep, Write, Edit, AskUserQuestion +allowed-tools: AskUserQuestion, Read, Write, Edit, Glob, Grep argument-hint: "[CR 名称或编号] [gate1|gate2|gate3|intent|change|risk|autonomy|timeline|arch]" model: haiku --- diff --git a/tests/integration/test_plugin_loading.sh b/tests/integration/test_plugin_loading.sh index 75b36a0..4d1b144 100755 --- a/tests/integration/test_plugin_loading.sh +++ b/tests/integration/test_plugin_loading.sh @@ -43,15 +43,18 @@ else echo -e "${GREEN}PASS${NC}" fi -# ── TC-PL-02: All 14 skills discovered ──────────────────────────────── -echo -n "TC-PL-02: All 13 skills discovered... " +# ── TC-PL-02: All 19 skills discovered ──────────────────────────────── +echo -n "TC-PL-02: All 19 skills discovered... " EXPECTED_SKILLS=( + "pace-biz" "pace-change" "pace-dev" "pace-feedback" + "pace-guard" "pace-init" "pace-learn" + "pace-next" "pace-plan" "pace-pulse" "pace-release" @@ -59,7 +62,10 @@ EXPECTED_SKILLS=( "pace-review" "pace-role" "pace-status" + "pace-sync" + "pace-test" "pace-theory" + "pace-trace" ) # Check that skill directories with SKILL.md exist @@ -71,7 +77,7 @@ for skill in "${EXPECTED_SKILLS[@]}"; do done if [ ${#MISSING_SKILLS[@]} -eq 0 ]; then - echo -e "${GREEN}PASS${NC} (14/14 skills have SKILL.md)" + echo -e "${GREEN}PASS${NC} (19/19 skills have SKILL.md)" else echo -e "${RED}FAIL${NC}" echo " Missing skills: ${MISSING_SKILLS[*]}" From 4b5af76689b1015545c42996a1e3db9e3cf980ad Mon Sep 17 00:00:00 2001 From: Sunny <277479420@qq.com> Date: Sat, 14 Mar 2026 10:29:31 +0800 Subject: [PATCH 03/70] =?UTF-8?q?fix(skills,tests):=20=E4=BD=8E=E4=BC=98?= =?UTF-8?q?=E5=85=88=E7=BA=A7=E4=BF=AE=E5=A4=8D=E2=80=94=E2=80=94NOT=20for?= =?UTF-8?q?=20=E6=8E=92=E9=99=A4=E6=89=A9=E5=B1=95=20+=20=E6=A0=87?= =?UTF-8?q?=E9=A2=98=E5=89=8D=E7=BC=80=20+=20Hook=20=E6=B5=8B=E8=AF=95?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit L2: pace-learn 标题补齐 / 前缀(# pace-learn → # /pace-learn) L4: 5 个 Skill 补齐 NOT for 排除条件: - pace-test: NOT for /pace-dev, /pace-review - pace-release: NOT for /pace-sync, /pace-dev - pace-retro: NOT for /pace-next, /pace-status - pace-role: NOT for /pace-status, /pace-theory - pace-theory: NOT for /pace-trace, /pace-dev Bug: test_pulse_counter.mjs 断言标签与 Hook 输出不匹配 - 测试检查 devpace:pulse-reminder(旧标签) - Hook 实际输出 devpace:write-volume(重构后标签) - 修复:测试断言对齐 Hook 实际输出 验证:validate-all.sh 全绿(含 Hook 9/9 pass),NOT for 覆盖 16/19 Co-Authored-By: Claude Opus 4.6 --- skills/pace-learn/SKILL.md | 2 +- skills/pace-release/SKILL.md | 2 +- skills/pace-retro/SKILL.md | 2 +- skills/pace-role/SKILL.md | 2 +- skills/pace-test/SKILL.md | 2 +- skills/pace-theory/SKILL.md | 2 +- tests/hooks/test_pulse_counter.mjs | 4 ++-- 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/skills/pace-learn/SKILL.md b/skills/pace-learn/SKILL.md index 2f03064..7cea76a 100644 --- a/skills/pace-learn/SKILL.md +++ b/skills/pace-learn/SKILL.md @@ -5,7 +5,7 @@ model: sonnet argument-hint: "[note|list|stats|export] [参数]" --- -# pace-learn — 经验积累与知识管理 +# /pace-learn — 经验积累与知识管理 devpace 的学习引擎。双模式运行: diff --git a/skills/pace-release/SKILL.md b/skills/pace-release/SKILL.md index 1b92685..9f3e43c 100644 --- a/skills/pace-release/SKILL.md +++ b/skills/pace-release/SKILL.md @@ -1,5 +1,5 @@ --- -description: Use when user says "发布", "部署", "上线", "release", "pace-release", or wants to create, deploy, or close a release. +description: Use when user says "发布", "部署", "上线", "release", "pace-release", or wants to create, deploy, or close a release. NOT for CI/CD pipeline management (use /pace-sync). NOT for code implementation (use /pace-dev). allowed-tools: AskUserQuestion, Read, Write, Edit, Glob, Bash argument-hint: "[create|deploy|verify|close|full|status|status history|changelog|version|tag|notes --role biz|ops|pm|branch|rollback]" model: sonnet diff --git a/skills/pace-retro/SKILL.md b/skills/pace-retro/SKILL.md index 2a84507..6b71b68 100644 --- a/skills/pace-retro/SKILL.md +++ b/skills/pace-retro/SKILL.md @@ -1,5 +1,5 @@ --- -description: Use when user says "回顾", "复盘", "度量", "retro", "总结", "数据分析", "DORA", "质量报告", "交付效率", "度量报告", "趋势", "中期检查", "对比", "预测", "forecast", "能按时交付吗", "交付概率", "瓶颈", "pace-retro", or at iteration end when reviewing progress and metrics. +description: Use when user says "回顾", "复盘", "度量", "retro", "总结", "数据分析", "DORA", "质量报告", "交付效率", "度量报告", "趋势", "中期检查", "对比", "预测", "forecast", "能按时交付吗", "交付概率", "瓶颈", "pace-retro", or at iteration end when reviewing progress and metrics. NOT for next-step recommendations (use /pace-next). NOT for current status overview (use /pace-status). allowed-tools: Read, Write, Edit, Glob, Bash argument-hint: "[update|focus <维度>|compare|history|mid|accept|forecast]" model: sonnet diff --git a/skills/pace-role/SKILL.md b/skills/pace-role/SKILL.md index 5ff634e..047b71f 100644 --- a/skills/pace-role/SKILL.md +++ b/skills/pace-role/SKILL.md @@ -1,5 +1,5 @@ --- -description: Use when user wants to switch output perspective (视角切换), says "切换角色/视角", "以XX视角", "pace-role", "作为产品经理", "作为运维", "换个角度看", or wants to view project from a different role perspective. +description: Use when user wants to switch output perspective (视角切换), says "切换角色/视角", "以XX视角", "pace-role", "作为产品经理", "作为运维", "换个角度看", or wants to view project from a different role perspective. NOT for project status overview (use /pace-status). NOT for understanding devpace concepts (use /pace-theory). allowed-tools: Read, Write, Glob argument-hint: "[biz 业务视角|pm 产品视角|dev 开发视角|tester 测试视角|ops 运维视角|auto 自动推断|compare 多视角快照]" model: haiku diff --git a/skills/pace-test/SKILL.md b/skills/pace-test/SKILL.md index a1377b9..a41b23c 100644 --- a/skills/pace-test/SKILL.md +++ b/skills/pace-test/SKILL.md @@ -1,5 +1,5 @@ --- -description: Use when user says "跑测试", "测试覆盖", "验证一下", "验收", "回归", "影响分析", "test", "verify", "accept", "coverage", "测试策略", /pace-test, or when test results, coverage gaps, or acceptance readiness are discussed. +description: Use when user says "跑测试", "测试覆盖", "验证一下", "验收", "回归", "影响分析", "test", "verify", "accept", "coverage", "测试策略", /pace-test, or when test results, coverage gaps, or acceptance readiness are discussed. NOT for code implementation (use /pace-dev). NOT for code review or approval (use /pace-review). allowed-tools: AskUserQuestion, Read, Write, Edit, Glob, Grep, Bash argument-hint: "[accept|strategy|coverage|impact|report|generate|...] [目标]" model: sonnet diff --git a/skills/pace-theory/SKILL.md b/skills/pace-theory/SKILL.md index 61dc628..e978675 100644 --- a/skills/pace-theory/SKILL.md +++ b/skills/pace-theory/SKILL.md @@ -1,5 +1,5 @@ --- -description: Use when user asks "为什么", "怎么理解", "概念", "理论", "方法论", "BizDevOps", "原理", "什么是 BR", "什么是 PF", "CR 是什么意思", "价值链", "状态机原理", "追溯", "闭环", "度量", "MoS", "成效指标", "设计决策", "pace-theory", or wants to understand devpace concepts, behavior rationale, or methodology. +description: Use when user asks "为什么", "怎么理解", "概念", "理论", "方法论", "BizDevOps", "原理", "什么是 BR", "什么是 PF", "CR 是什么意思", "价值链", "状态机原理", "追溯", "闭环", "度量", "MoS", "成效指标", "设计决策", "pace-theory", or wants to understand devpace concepts, behavior rationale, or methodology. NOT for specific CR decision audit trail (use /pace-trace). NOT for code implementation (use /pace-dev). allowed-tools: Read, Glob, Grep argument-hint: "[model|objects|spaces|rules|trace|topic|metrics|loops|change|mapping|decisions|vs-devops|sdd|why|all|<关键词>]" model: haiku diff --git a/tests/hooks/test_pulse_counter.mjs b/tests/hooks/test_pulse_counter.mjs index 020092e..526637c 100644 --- a/tests/hooks/test_pulse_counter.mjs +++ b/tests/hooks/test_pulse_counter.mjs @@ -86,7 +86,7 @@ describe('pulse-counter: counting and reminders', () => { const result = await runHook({}, projectDir); assert.equal(result.exitCode, 0); assert.equal(readFileSync(counterPath, 'utf-8'), '10'); - assert.ok(result.stdout.includes('devpace:pulse-reminder'), 'Should output pulse reminder at 10'); + assert.ok(result.stdout.includes('devpace:write-volume'), 'Should output pulse reminder at 10'); }); it('outputs pulse reminder at count 20', async () => { @@ -94,7 +94,7 @@ describe('pulse-counter: counting and reminders', () => { writeFileSync(counterPath, '19'); const result = await runHook({}, projectDir); assert.equal(result.exitCode, 0); - assert.ok(result.stdout.includes('devpace:pulse-reminder'), 'Should output pulse reminder at 20'); + assert.ok(result.stdout.includes('devpace:write-volume'), 'Should output pulse reminder at 20'); }); it('no reminder at non-10 counts', async () => { From 629a3125b6310e8e231eadb29f1255a3840a1daa Mon Sep 17 00:00:00 2001 From: Sunny <277479420@qq.com> Date: Sat, 14 Mar 2026 10:40:19 +0800 Subject: [PATCH 04/70] =?UTF-8?q?refactor(skills):=20L1+L7=E2=80=94?= =?UTF-8?q?=E2=80=94pace-biz=20=E8=BE=93=E5=87=BA=E7=A4=BA=E4=BE=8B?= =?UTF-8?q?=E5=88=86=E6=8B=86=20+=20=E6=8E=A8=E8=8D=90=E4=BD=BF=E7=94=A8?= =?UTF-8?q?=E6=B5=81=E7=A8=8B=E8=A1=A5=E9=BD=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit L1: pace-biz SKILL.md 输出示例(8 个子命令模板)移入 biz-procedures-output.md,SKILL.md 193→110 行(-43%) L7: pace-release 添加"推荐使用流程"(create→deploy→verify→close) pace-guard 添加"推荐使用流程"(scan→monitor→resolve→trends→report) Co-Authored-By: Claude Opus 4.6 --- skills/pace-biz/SKILL.md | 84 +---------------------- skills/pace-biz/biz-procedures-output.md | 85 ++++++++++++++++++++++++ skills/pace-guard/SKILL.md | 9 +++ skills/pace-release/SKILL.md | 10 +++ 4 files changed, 105 insertions(+), 83 deletions(-) create mode 100644 skills/pace-biz/biz-procedures-output.md diff --git a/skills/pace-biz/SKILL.md b/skills/pace-biz/SKILL.md index a24eea3..e3c712f 100644 --- a/skills/pace-biz/SKILL.md +++ b/skills/pace-biz/SKILL.md @@ -103,90 +103,8 @@ $ARGUMENTS: ## 输出 -### 所有子命令的通用输出原则 - - **渐进暴露**:默认输出简洁摘要,`--detail` 展示完整信息 - **操作确认**:写入操作前展示变更预览,用户确认后执行 - **追溯链**:每次创建实体时展示其在价值链中的位置 -### opportunity 输出 - -``` -已捕获业务机会:OPP-xxx — [描述] -来源:[类型]([详情]) -状态:评估中 -→ 下一步:/pace-biz epic OPP-xxx 评估并转化为 Epic -``` - -### epic 输出 - -``` -已创建专题:EPIC-xxx — [名称] -关联:OBJ-x([目标])← OPP-xxx(如有) -MoS:[指标列表] -→ 下一步:/pace-biz decompose EPIC-xxx 分解为业务需求 -``` - -### decompose 输出 - -``` -已分解 [EPIC-xxx|BR-xxx]: -├── BR-001:[名称] P0 -├── BR-002:[名称] P1 -└── BR-003:[名称] P2 -→ 下一步:/pace-change add 补充 PF 或 /pace-dev 开始开发 -``` - -### align 输出 - -``` -战略对齐度报告: -- OBJ 覆盖率:N/M OBJ 有 Epic 覆盖 -- 孤立实体:[列表] -- 对齐建议:[建议] -``` - -### view 输出 - -``` -业务全景: -OPP-001(评估中) -OPP-002 → EPIC-001(进行中) - ├── BR-001 → PF-001 → CR-001 🔄 - └── BR-002 → PF-002(待开始) -OPP-003 → EPIC-002(规划中) -``` - -### discover 输出 - -``` -已从发现会话创建: -- 1 个业务机会(OPP-xxx) -- 1 个专题(EPIC-xxx) -- N 个业务需求(BR-xxx ~ BR-xxx) -- M 个产品功能(PF-xxx ~ PF-xxx) -→ /pace-biz decompose EPIC-xxx 继续细化 -→ /pace-plan next 排入迭代 -``` - -### import 输出 - -``` -导入完成(来自 N 个文件): -- 新增:X 个 BR + Y 个 PF -- 丰富:Z 个已有实体 -- 跳过:W 个重复项 -→ /pace-biz align 检查战略对齐度 -→ /pace-plan next 排入迭代 -``` - -### infer 输出 - -``` -代码库推断完成: -- 新增追踪:X 个产品功能 -- 技术债务:Y 个待处理项 -- 未实现确认:Z 个功能状态已更新 -→ /pace-biz align 检查战略对齐度 -→ /pace-dev 开始处理优先项 -``` +各子命令输出格式模板见 `biz-procedures-output.md`。 diff --git a/skills/pace-biz/biz-procedures-output.md b/skills/pace-biz/biz-procedures-output.md new file mode 100644 index 0000000..5e836ec --- /dev/null +++ b/skills/pace-biz/biz-procedures-output.md @@ -0,0 +1,85 @@ +# 业务规划域输出格式规程 + +> **职责**:定义 /pace-biz 各子命令的输出格式模板。 + +## opportunity 输出 + +``` +已捕获业务机会:OPP-xxx — [描述] +来源:[类型]([详情]) +状态:评估中 +→ 下一步:/pace-biz epic OPP-xxx 评估并转化为 Epic +``` + +## epic 输出 + +``` +已创建专题:EPIC-xxx — [名称] +关联:OBJ-x([目标])← OPP-xxx(如有) +MoS:[指标列表] +→ 下一步:/pace-biz decompose EPIC-xxx 分解为业务需求 +``` + +## decompose 输出 + +``` +已分解 [EPIC-xxx|BR-xxx]: +├── BR-001:[名称] P0 +├── BR-002:[名称] P1 +└── BR-003:[名称] P2 +→ 下一步:/pace-change add 补充 PF 或 /pace-dev 开始开发 +``` + +## align 输出 + +``` +战略对齐度报告: +- OBJ 覆盖率:N/M OBJ 有 Epic 覆盖 +- 孤立实体:[列表] +- 对齐建议:[建议] +``` + +## view 输出 + +``` +业务全景: +OPP-001(评估中) +OPP-002 → EPIC-001(进行中) + ├── BR-001 → PF-001 → CR-001 🔄 + └── BR-002 → PF-002(待开始) +OPP-003 → EPIC-002(规划中) +``` + +## discover 输出 + +``` +已从发现会话创建: +- 1 个业务机会(OPP-xxx) +- 1 个专题(EPIC-xxx) +- N 个业务需求(BR-xxx ~ BR-xxx) +- M 个产品功能(PF-xxx ~ PF-xxx) +→ /pace-biz decompose EPIC-xxx 继续细化 +→ /pace-plan next 排入迭代 +``` + +## import 输出 + +``` +导入完成(来自 N 个文件): +- 新增:X 个 BR + Y 个 PF +- 丰富:Z 个已有实体 +- 跳过:W 个重复项 +→ /pace-biz align 检查战略对齐度 +→ /pace-plan next 排入迭代 +``` + +## infer 输出 + +``` +代码库推断完成: +- 新增追踪:X 个产品功能 +- 技术债务:Y 个待处理项 +- 未实现确认:Z 个功能状态已更新 +→ /pace-biz align 检查战略对齐度 +→ /pace-dev 开始处理优先项 +``` diff --git a/skills/pace-guard/SKILL.md b/skills/pace-guard/SKILL.md index 3302493..1ddc0a5 100644 --- a/skills/pace-guard/SKILL.md +++ b/skills/pace-guard/SKILL.md @@ -11,6 +11,15 @@ agent: pace-analyst 统一管理开发全生命周期的风险:从编码前的 Pre-flight 扫描,到开发中的实时监控,再到跨迭代的趋势分析——让风险可见、可追踪、可解决。风险评估覆盖 Epic 级别(Epic 范围风险影响其下所有 BR/PF/CR)。 +## 推荐使用流程 + +``` +编码前预检: scan(L/XL CR 意图检查点自动触发) +开发中监控: monitor(pace-pulse 周期性触发) +问题解决: resolve RISK-xxx mitigated +迭代回顾: trends → report +``` + ## 子命令 | 子命令 | 用途 | 输入 | 自动触发 | diff --git a/skills/pace-release/SKILL.md b/skills/pace-release/SKILL.md index 9f3e43c..a6a7e58 100644 --- a/skills/pace-release/SKILL.md +++ b/skills/pace-release/SKILL.md @@ -14,6 +14,16 @@ agent: pace-engineer 管理 Release 生命周期:收集候选变更 → 创建 Release → 追踪部署 → 验证 → 关闭。支持 Changelog、版本 bump、Git Tag、GitHub Release、Release Notes 和发布分支管理。 +## 推荐使用流程 + +``` +标准发布: create → deploy → verify → close +快速查看: status(或无参数启动引导向导) +单步操作: changelog / version / tag / notes(独立使用) +回滚处理: rollback → 创建 hotfix CR → 新 create +历史回顾: status history +``` + ## 输入 $ARGUMENTS: From e459477280ad62c5c2b9c577b75b8c6f213fdaa6 Mon Sep 17 00:00:00 2001 From: Sunny <277479420@qq.com> Date: Sat, 14 Mar 2026 10:50:00 +0800 Subject: [PATCH 05/70] =?UTF-8?q?refactor(hooks):=20=E6=9C=80=E5=B0=8F?= =?UTF-8?q?=E8=87=AA=E6=B2=BB=E5=8E=9F=E5=88=99=E4=BC=98=E5=8C=96=E2=80=94?= =?UTF-8?q?=E2=80=946=20=E9=A1=B9=20Hook=20=E8=A1=8C=E4=B8=BA=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit P0: 收窄 explore 模式阻断范围——仅阻断 state.md 直接修改和 CR 状态推进(developing/verifying/in_review),放行管理 Skill (pace-change/pace-biz/pace-plan)的合法 .devpace/ 写入。 新增 isStateEscalation() 函数精确判断推进模式专属状态。 P1: 去除 pace-dev-scope-check 的 Gate 3 双重阻断,委托给 全局 pre-tool-use.mjs(参照 pace-review 成功模式)。 P2: 精简 post-cr-update 职责——移除 7 步管道编排和 .learn-pending 文件写入,改为单行信号引用。学习触发器简化为单行输出。 P2: 修正 advisory hook 指令性语言——sync-push merged 场景 Auto-execute → Suggest。 P3: 收紧 intent-detect 关键词——移除独立的「恢复」,增加技术 上下文过滤(git/代码操作不误触发变更管理流程)。 P3: pulse-counter 改为 async,消除 PostToolUse 中唯一的同步瓶颈。 测试:126 tests pass, 0 fail(新增 isStateEscalation 8 条单元测试 + explore 模式 4 条场景测试 + intent-detect 2 条误触发测试)。 Co-Authored-By: Claude Opus 4.6 --- hooks/hooks.json | 1 + hooks/intent-detect.mjs | 7 ++- hooks/lib/utils.mjs | 11 +++++ hooks/pace-dev-scope-check.mjs | 23 ++++----- hooks/post-cr-update.mjs | 58 +++-------------------- hooks/pre-tool-use.mjs | 27 +++++++---- hooks/sync-push.mjs | 6 +-- tests/hooks/test_intent_detect.mjs | 14 +++++- tests/hooks/test_pace_dev_scope_check.mjs | 22 ++------- tests/hooks/test_pre_tool_use.mjs | 44 +++++++++++++++-- tests/hooks/test_sync_push.mjs | 2 +- tests/hooks/test_utils.mjs | 40 ++++++++++++++++ 12 files changed, 153 insertions(+), 102 deletions(-) diff --git a/hooks/hooks.json b/hooks/hooks.json index 4a019be..9316a6b 100644 --- a/hooks/hooks.json +++ b/hooks/hooks.json @@ -40,6 +40,7 @@ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/pulse-counter.mjs", "timeout": 5, + "async": true, "statusMessage": "devpace pulse counter" }, { diff --git a/hooks/intent-detect.mjs b/hooks/intent-detect.mjs index 143fff9..bb9fa21 100755 --- a/hooks/intent-detect.mjs +++ b/hooks/intent-detect.mjs @@ -26,6 +26,9 @@ const userPrompt = input?.content ?? ''; // Change management trigger words // Synced with skills/pace-change/SKILL.md description (authority source) // Categories: add / pause / resume / reprioritize / modify + English variants +// Technical context words — if the prompt is about code/git operations, skip change detection +const techContextPattern = /注释|缩进|格式化?|配置文件|代码风格|git\s|stash|commit|branch|merge|rebase|checkout/; + const triggerPattern = new RegExp([ // --- add --- '加一个', '加需求', '新增需求', '插入', '还需要', '补一个', '追加', @@ -33,7 +36,7 @@ const triggerPattern = new RegExp([ '不做了', '先不搞', '砍掉', '搁置', '放一放', '暂停', '停掉', '先放着', '不要这个功能了', '延后', // --- resume --- - '恢复之前', '恢复', '重新开始', '捡回来', '继续之前', + '恢复之前', '重新开始', '捡回来', '继续之前', // --- reprioritize --- '优先级', '先做这个', '提前', '排到前面', '优先', '调个顺序', // --- modify --- @@ -44,7 +47,7 @@ const triggerPattern = new RegExp([ '\\bdefer\\b', '\\bshelve\\b', '\\breprioritize\\b', ].join('|')); -if (triggerPattern.test(userPrompt)) { +if (triggerPattern.test(userPrompt) && !techContextPattern.test(userPrompt)) { console.log('devpace:change-detected Change intent detected in user prompt. Follow devpace-rules.md §9 change management workflow: classify → impact analysis → confirmation → execute.'); } diff --git a/hooks/lib/utils.mjs b/hooks/lib/utils.mjs index 375a9de..dff995d 100644 --- a/hooks/lib/utils.mjs +++ b/hooks/lib/utils.mjs @@ -112,6 +112,17 @@ export function isStateChangeToApproved(content) { return /\*\*状态\*\*[::]\s*approved/.test(content); } +/** + * Check if write content sets CR state to an advance-mode-only value. + * These states (developing, verifying, in_review) represent active progress + * and should not be set in explore mode — use advance mode via /pace-dev. + * States like created and paused are allowed in explore mode (pace-change needs them). + */ +export function isStateEscalation(content) { + if (!content) return false; + return /\*\*状态\*\*[::]\s*(developing|verifying|in_review)/.test(content); +} + /** * Read the last event from a CR file's event table. * Parses the structured event format: | timestamp | event_type | actor | note | handoff | diff --git a/hooks/pace-dev-scope-check.mjs b/hooks/pace-dev-scope-check.mjs index 89c991e..6415f69 100755 --- a/hooks/pace-dev-scope-check.mjs +++ b/hooks/pace-dev-scope-check.mjs @@ -3,22 +3,22 @@ * devpace pace-dev scope check — fast command Hook replacing LLM prompt Hook * * Replaces the slow prompt-type Hook (~15s LLM per call) with a fast - * programmatic check (~5ms) for scope validation and Gate 3 enforcement. + * programmatic check (~5ms) for scope validation during /pace-dev. * * Checks: - * 1. Gate 3: Block automated state change to approved (defense-in-depth) - * 2. Scope validation: Is target file within the active CR's scope? - * 3. Scope drift warning: Advisory for out-of-scope writes + * 1. CR file writes: always in scope during development (Gate 3 delegated to global hook) + * 2. .devpace/ management files: always in scope + * 3. Scope validation: Is target file within the active CR's scope? + * 4. Scope drift warning: Advisory for out-of-scope writes * * Exit codes: * 0 = allow (in scope or advisory warning) - * 2 = block (Gate 3 violation) */ import { readFileSync, existsSync } from 'node:fs'; import { - readStdinJson, getProjectDir, extractFilePath, extractWriteContent, - isCrFile, isStateChangeToApproved, isDevpaceFile + readStdinJson, getProjectDir, extractFilePath, + isCrFile, isDevpaceFile } from './lib/utils.mjs'; const input = await readStdinJson(); @@ -35,14 +35,9 @@ if (!filePath) { process.exit(0); } -// ── CHECK 1: Gate 3 — block automated approved state change ────── +// ── CHECK 1: CR file writes — always in scope during development ── +// Gate 3 (approved blocking) is enforced by global pre-tool-use.mjs — no duplication needed. if (isCrFile(filePath, backlogDir)) { - const newContent = extractWriteContent(input); - if (isStateChangeToApproved(newContent)) { - console.error('devpace:blocked Gate 3 要求人类审批。不允许自动将 CR 状态变更为 approved。'); - process.exit(2); - } - // CR file writes are always in scope during development process.exit(0); } diff --git a/hooks/post-cr-update.mjs b/hooks/post-cr-update.mjs index afc17b1..f910b0d 100755 --- a/hooks/post-cr-update.mjs +++ b/hooks/post-cr-update.mjs @@ -3,14 +3,14 @@ * devpace PostToolUse hook — detect CR merged state and trigger knowledge pipeline * * Purpose: After a Write/Edit to a CR file, check if the CR transitioned to 'merged'. - * If so, output a reminder for Claude to trigger the post-merge pipeline (§11 aligned): - * 7-step pipeline for merged CR processing, with conditional step 7 for external sync. + * If so, output a signal reference for Claude to execute the §11 post-merge pipeline. + * Also detects gate failures and rejections as learning triggers. * * This is an advisory hook (exit 0), not blocking. */ -import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs'; -import { basename, dirname } from 'node:path'; +import { existsSync } from 'node:fs'; +import { basename } from 'node:path'; import { readStdinJson, getProjectDir, extractFilePath, isCrFile, readCrState, getLastEvent } from './lib/utils.mjs'; const input = await readStdinJson(); @@ -36,63 +36,19 @@ if (existsSync(filePath)) { const crName = basename(filePath, '.md'); if (currentState === 'merged') { - // Build pipeline message — steps 1-6 always present (§11 aligned) - const steps = [ - '1) Cascading updates (PF + project.md + state.md + iterations + Release)', - '2) pace-learn knowledge extraction', - '3) dashboard.md incremental metrics', - '4) PF completion → release note', - '5) Iteration completion check (>90% → suggest retro)', - '6) First-CR review (teaching dedup)', - ]; - - // Step 7: conditional — only if sync-mapping exists and CR has external link - const syncMappingPath = `${projectDir}/.devpace/integrations/sync-mapping.md`; - if (existsSync(syncMappingPath)) { - try { - const content = readFileSync(filePath, 'utf-8'); - const hasExternalLink = /\*\*外部关联\*\*[::]/.test(content); - if (hasExternalLink) { - steps.push(`7) External sync push: auto-execute /pace-sync push ${crName}`); - } - } catch { - // Read error — skip step 7 - } - } - - console.log(`devpace:post-merge ${crName} merged. Execute post-merge pipeline: ${steps.join(' ')}`); - - // Write .learn-pending flag for session-start reminder - try { - const pendingPath = `${projectDir}/.devpace/.learn-pending`; - mkdirSync(dirname(pendingPath), { recursive: true }); - const existing = existsSync(pendingPath) ? readFileSync(pendingPath, 'utf-8').trim() : ''; - const entry = `${crName} ${new Date().toISOString()}`; - const newContent = existing ? `${existing}\n${entry}` : entry; - writeFileSync(pendingPath, newContent + '\n', 'utf-8'); - } catch { - // Non-critical — learn-pending write failure doesn't block pipeline - } + console.log(`devpace:post-merge ${crName} merged. Execute §11 post-merge pipeline.`); } // Gate fail learning trigger — gate_fail is a valuable learning opportunity const recentEvent = getLastEvent(filePath); if (recentEvent && (recentEvent.type === 'gate1_fail' || recentEvent.type === 'gate2_fail')) { const gateNum = recentEvent.type.includes('1') ? '1' : '2'; - console.log([ - `devpace:learn-trigger ${crName} Gate ${gateNum} 失败是学习机会。`, - ` 建议: 调用 /pace-learn 提取 Gate ${gateNum} 失败原因`, - ` 关注: 失败的检查项是否应该调整阈值,或 Claude 有可避免的盲区`, - ].join('\n')); + console.log(`devpace:learn-trigger ${crName} Gate ${gateNum} failed. Consider /pace-learn to extract lessons.`); } // Rejected learning trigger — human rejection reveals understanding gaps if (recentEvent && recentEvent.type === 'rejected') { - console.log([ - `devpace:learn-trigger ${crName} 人类打回是理解差距的信号。`, - ` 建议: 调用 /pace-learn 分析打回原因`, - ` 关注: Claude 的意图理解是否与人类预期一致`, - ].join('\n')); + console.log(`devpace:learn-trigger ${crName} rejected. Consider /pace-learn to analyze gap.`); } } diff --git a/hooks/pre-tool-use.mjs b/hooks/pre-tool-use.mjs index f00cb78..c56524b 100755 --- a/hooks/pre-tool-use.mjs +++ b/hooks/pre-tool-use.mjs @@ -5,18 +5,21 @@ * Purpose: Enforce devpace iron rules at the mechanism level, not just text-based rules. * * Enforcement levels: - * 1. BLOCKING (exit 2): Explore mode writes to .devpace/, Gate 3 bypass attempts + * 1. BLOCKING (exit 2): Explore mode state escalation, Gate 3 bypass attempts * 2. ADVISORY (exit 0): Gate 1/2 reminders during normal development flow * * Iron rules enforced: - * - Explore mode: no writes to .devpace/ (devpace-rules.md §2) + * - Explore mode: block state.md writes and CR state escalation to advance-mode + * states (developing/verifying/in_review). Allow other .devpace/ writes so + * management Skills (pace-change, pace-biz, pace-plan) can operate. * - Gate 3: human approval required, no automated state change to approved (devpace-rules.md §2) */ import { existsSync } from 'node:fs'; import { readStdinJson, getProjectDir, extractFilePath, extractWriteContent, - isCrFile, readCrState, isDevpaceFile, isAdvanceMode, isStateChangeToApproved + isCrFile, readCrState, isDevpaceFile, isAdvanceMode, isStateChangeToApproved, + isStateEscalation } from './lib/utils.mjs'; const input = await readStdinJson(); @@ -31,13 +34,19 @@ if (!existsSync(backlogDir)) { const filePath = extractFilePath(input); // ── ENFORCEMENT 1: Explore mode protection ────────────────────────── -// Iron rule: explore mode must not write to .devpace/ files +// Narrowed scope: only block high-risk state operations in explore mode. +// Management Skills (pace-change, pace-biz, pace-plan) need to write to +// .devpace/ files even without an active CR, so we only block: +// 1. state.md direct modification — progress state shouldn't change in explore mode +// 2. CR state escalation — setting developing/verifying/in_review requires advance mode if (isDevpaceFile(filePath) && !isAdvanceMode(projectDir)) { - // Allow writes to .devpace/rules/ (configuration, not state) - // Allow writes to .devpace/context.md (tech convention tracking) - const isConfigFile = filePath.includes('.devpace/rules/') || filePath.includes('.devpace/context.md'); - if (!isConfigFile) { - console.error('devpace:blocked 探索模式下不允许修改 .devpace/ 状态文件。请先进入推进模式(说"帮我实现/修改 X")再修改。'); + const isStateMd = filePath.endsWith('/state.md') || filePath.endsWith('/.devpace/state.md'); + const isCrStateEsc = isCrFile(filePath, backlogDir) + && existsSync(filePath) + && isStateEscalation(extractWriteContent(input)); + + if (isStateMd || isCrStateEsc) { + console.error('devpace:blocked 探索模式下不允许直接修改进度状态。请先进入推进模式(说"帮我实现/修改 X")再修改。'); process.exit(2); } } diff --git a/hooks/sync-push.mjs b/hooks/sync-push.mjs index ca977e3..3bb9341 100755 --- a/hooks/sync-push.mjs +++ b/hooks/sync-push.mjs @@ -6,7 +6,7 @@ * so ordinary edits that don't change state are silently ignored. * * - State unchanged → silent exit (no noise) - * - State changed to merged → directive language (auto-execute) + * - State changed to merged → advisory suggestion (suggest sync push) * - State changed to other value → advisory suggestion * * Advisory only (exit 0) — never blocks workflow. @@ -81,8 +81,8 @@ try { const linkText = linkMatch ? linkMatch[1] : '外部实体'; if (newState === 'merged') { - // Directive language for merged — §11 step 7 close-loop - console.log(`devpace:sync-push ${crName} state transition: ${oldState || '(new)'}→merged, linked to ${linkText}. Auto-execute: /pace-sync push ${crName} (§11 step 7 — close Issue + done label + completion summary)`); + // Advisory language for merged — §11 step 7 close-loop + console.log(`devpace:sync-push ${crName} state transition: ${oldState || '(new)'}→merged, linked to ${linkText}. Suggest: /pace-sync push ${crName} (§11 step 7 — close Issue + done label + completion summary)`); } else { // Advisory suggestion for other transitions console.log(`devpace:sync-push ${crName} state transition: ${oldState || '(new)'}→${newState}, linked to ${linkText}. Consider running /pace-sync push to sync status.`); diff --git a/tests/hooks/test_intent_detect.mjs b/tests/hooks/test_intent_detect.mjs index dc665e0..67a2652 100644 --- a/tests/hooks/test_intent_detect.mjs +++ b/tests/hooks/test_intent_detect.mjs @@ -71,7 +71,7 @@ describe('intent-detect: change trigger words', () => { const triggerWords = [ '不做了', '先不搞', '加一个', '改一下', '优先级', '延后', '提前', '砍掉', - '插入', '新增需求', '先做这个', '恢复之前' + '插入', '新增需求', '先做这个', '恢复之前', ]; for (const word of triggerWords) { @@ -98,4 +98,16 @@ describe('intent-detect: change trigger words', () => { const result = await runHook({ content: '不做了这个任务' }, projectDir); assert.equal(result.exitCode, 0, 'Intent detect should never block (exit 2)'); }); + + it('skips detection when technical context words present', async () => { + const result = await runHook({ content: '恢复之前的 git stash' }, projectDir); + assert.equal(result.exitCode, 0); + assert.equal(result.stdout, '', 'Should not trigger when tech context detected'); + }); + + it('skips detection for code formatting requests', async () => { + const result = await runHook({ content: '帮我格式化一下代码缩进' }, projectDir); + assert.equal(result.exitCode, 0); + assert.equal(result.stdout, '', 'Should not trigger for code formatting'); + }); }); diff --git a/tests/hooks/test_pace_dev_scope_check.mjs b/tests/hooks/test_pace_dev_scope_check.mjs index 3ca40a4..cb514b3 100644 --- a/tests/hooks/test_pace_dev_scope_check.mjs +++ b/tests/hooks/test_pace_dev_scope_check.mjs @@ -102,15 +102,15 @@ describe('pace-dev-scope-check: no file_path', () => { }); }); -// ── Tests: Gate 3 — block automated approved state change ────────── +// ── Tests: CR writes — Gate 3 delegated to global hook ─────────────── -describe('pace-dev-scope-check: Gate 3 enforcement', () => { +describe('pace-dev-scope-check: CR writes (Gate 3 delegated to global hook)', () => { let projectDir; beforeEach(() => { projectDir = createTmpProject(); }); afterEach(() => { cleanupDir(projectDir); }); - it('blocks state change to approved on CR file (exit 2)', async () => { + it('allows all CR writes including approved state (Gate 3 handled by global hook)', async () => { const crPath = writeCr(projectDir, '001', '# CR-001\n\n- **状态**:in_review\n'); const input = { tool_input: { @@ -119,21 +119,7 @@ describe('pace-dev-scope-check: Gate 3 enforcement', () => { } }; const result = await runHook(input, projectDir); - assert.equal(result.exitCode, 2, `Expected exit 2 (Gate 3 block) but got ${result.exitCode}`); - assert.ok(result.stderr.includes('Gate 3'), 'Should mention Gate 3'); - }); - - it('blocks Edit with new_string changing to approved (exit 2)', async () => { - const crPath = writeCr(projectDir, '002', '# CR-002\n\n- **状态**:in_review\n'); - const input = { - tool_input: { - file_path: crPath, - old_string: '- **状态**:in_review', - new_string: '- **状态**:approved' - } - }; - const result = await runHook(input, projectDir); - assert.equal(result.exitCode, 2); + assert.equal(result.exitCode, 0, 'Gate 3 is no longer enforced here — delegated to global pre-tool-use.mjs'); }); it('allows non-approved state change on CR file (exit 0)', async () => { diff --git a/tests/hooks/test_pre_tool_use.mjs b/tests/hooks/test_pre_tool_use.mjs index 5714260..25fc6ee 100644 --- a/tests/hooks/test_pre_tool_use.mjs +++ b/tests/hooks/test_pre_tool_use.mjs @@ -98,17 +98,55 @@ describe('pre-tool-use: explore mode enforcement', () => { assert.ok(result.stderr.includes('devpace:blocked'), 'Should output blocked message'); }); - it('blocks write to .devpace/backlog/CR-001.md in explore mode (exit 2)', async () => { + it('allows write to CR file in explore mode when no state escalation', async () => { const crPath = join(projectDir, '.devpace', 'backlog', 'CR-001.md'); writeFileSync(crPath, '# CR-001\n\n- **状态**:created\n'); const input = { tool_input: { file_path: crPath, - content: '# CR-001 modified' + content: '# CR-001\n\n- **状态**:created\n- **优先级**:high\n' } }; const result = await runHook(input, projectDir); - assert.equal(result.exitCode, 2, `Expected exit 2 but got ${result.exitCode}`); + assert.equal(result.exitCode, 0, `Expected exit 0 (management Skill writes allowed) but got ${result.exitCode}`); + }); + + it('blocks CR state escalation to developing in explore mode (exit 2)', async () => { + const crPath = join(projectDir, '.devpace', 'backlog', 'CR-001.md'); + writeFileSync(crPath, '# CR-001\n\n- **状态**:created\n'); + const input = { + tool_input: { + file_path: crPath, + content: '# CR-001\n\n- **状态**:developing\n' + } + }; + const result = await runHook(input, projectDir); + assert.equal(result.exitCode, 2, `Expected exit 2 (state escalation blocked) but got ${result.exitCode}`); + assert.ok(result.stderr.includes('devpace:blocked'), 'Should output blocked message'); + }); + + it('allows new CR creation in explore mode (file does not exist)', async () => { + const crPath = join(projectDir, '.devpace', 'backlog', 'CR-NEW.md'); + // File does not exist — new CR creation by pace-change + const input = { + tool_input: { + file_path: crPath, + content: '# CR-NEW\n\n- **状态**:created\n' + } + }; + const result = await runHook(input, projectDir); + assert.equal(result.exitCode, 0, `Expected exit 0 (new CR creation allowed) but got ${result.exitCode}`); + }); + + it('allows write to .devpace/project.md in explore mode', async () => { + const input = { + tool_input: { + file_path: join(projectDir, '.devpace', 'project.md'), + content: '# Project\n' + } + }; + const result = await runHook(input, projectDir); + assert.equal(result.exitCode, 0, 'Management files like project.md should be allowed in explore mode'); }); it('allows write to .devpace/rules/ in explore mode (config files)', async () => { diff --git a/tests/hooks/test_sync_push.mjs b/tests/hooks/test_sync_push.mjs index 557419d..2455f3d 100644 --- a/tests/hooks/test_sync_push.mjs +++ b/tests/hooks/test_sync_push.mjs @@ -175,7 +175,7 @@ describe('sync-push: merged transition with external link', () => { assert.ok(result.stdout.includes('sync-push'), 'Should include sync-push prefix'); assert.ok(result.stdout.includes('merged'), 'Should mention merged state'); assert.ok(result.stdout.includes('Issue #123'), 'Should include external link text'); - assert.ok(result.stdout.includes('Auto-execute'), 'Should use directive language for merged'); + assert.ok(result.stdout.includes('Suggest'), 'Should use advisory language for merged'); }); it('outputs directive for new CR (no cached state) transitioning to merged', async () => { diff --git a/tests/hooks/test_utils.mjs b/tests/hooks/test_utils.mjs index 8b0fb8e..0d5cbb8 100644 --- a/tests/hooks/test_utils.mjs +++ b/tests/hooks/test_utils.mjs @@ -17,6 +17,7 @@ import { isAdvanceMode, extractWriteContent, isStateChangeToApproved, + isStateEscalation, readSyncStateCache, updateSyncStateCache } from '../../hooks/lib/utils.mjs'; @@ -223,6 +224,45 @@ describe('isStateChangeToApproved', () => { }); }); +// ── isStateEscalation ───────────────────────────────────────────── + +describe('isStateEscalation', () => { + it('detects developing as escalation', () => { + assert.equal(isStateEscalation('- **状态**:developing'), true); + }); + + it('detects verifying as escalation', () => { + assert.equal(isStateEscalation('- **状态**:verifying'), true); + }); + + it('detects in_review as escalation', () => { + assert.equal(isStateEscalation('- **状态**:in_review'), true); + }); + + it('does not flag created (management Skill state)', () => { + assert.equal(isStateEscalation('- **状态**:created'), false); + }); + + it('does not flag paused (management Skill state)', () => { + assert.equal(isStateEscalation('- **状态**:paused'), false); + }); + + it('does not flag approved/merged', () => { + assert.equal(isStateEscalation('- **状态**:approved'), false); + assert.equal(isStateEscalation('- **状态**:merged'), false); + }); + + it('returns false for empty/null', () => { + assert.equal(isStateEscalation(''), false); + assert.equal(isStateEscalation(null), false); + assert.equal(isStateEscalation(undefined), false); + }); + + it('works with ASCII colon', () => { + assert.equal(isStateEscalation('- **状态**: developing'), true); + }); +}); + // ── readSyncStateCache ────────────────────────────────────────────── describe('readSyncStateCache', () => { From d06e7b3a3702df9994e0a1e2c876e9ef4feacad2 Mon Sep 17 00:00:00 2001 From: Sunny <277479420@qq.com> Date: Sat, 14 Mar 2026 11:03:46 +0800 Subject: [PATCH 06/70] =?UTF-8?q?refactor(hooks):=20skill=20=E7=BA=A7=20Ho?= =?UTF-8?q?ok=20=E8=84=9A=E6=9C=AC=E7=A7=BB=E5=85=A5=20hooks/skill/=20?= =?UTF-8?q?=E5=AD=90=E7=9B=AE=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将 3 个 skill 级 scope-check 脚本从 hooks/ 根目录移入 hooks/skill/, 与 14 个全局 Hook 脚本做目录分层,便于快速区分全局行为与 skill 特有行为。 变更: - git mv 3 个脚本到 hooks/skill/ - 修正 import 路径 ./lib/utils.mjs → ../lib/utils.mjs - 更新 3 个 SKILL.md 的 hook command 路径 - 新增 skill 级脚本的专用测试(exist/executable/shebang) - 更新 test_pace_init.py 和 test_pace_dev_scope_check.mjs 的路径引用 Co-Authored-By: Claude Opus 4.6 --- hooks/{ => skill}/pace-dev-scope-check.mjs | 2 +- hooks/{ => skill}/pace-init-scope-check.mjs | 2 +- hooks/{ => skill}/pace-review-scope-check.mjs | 2 +- skills/pace-dev/SKILL.md | 2 +- skills/pace-init/SKILL.md | 2 +- skills/pace-review/SKILL.md | 2 +- tests/hooks/test_pace_dev_scope_check.mjs | 4 +- tests/static/test_hooks.py | 37 ++++++++++++++++++- tests/static/test_pace_init.py | 2 +- 9 files changed, 45 insertions(+), 10 deletions(-) rename hooks/{ => skill}/pace-dev-scope-check.mjs (99%) rename hooks/{ => skill}/pace-init-scope-check.mjs (98%) rename hooks/{ => skill}/pace-review-scope-check.mjs (98%) diff --git a/hooks/pace-dev-scope-check.mjs b/hooks/skill/pace-dev-scope-check.mjs similarity index 99% rename from hooks/pace-dev-scope-check.mjs rename to hooks/skill/pace-dev-scope-check.mjs index 6415f69..c2056bb 100755 --- a/hooks/pace-dev-scope-check.mjs +++ b/hooks/skill/pace-dev-scope-check.mjs @@ -19,7 +19,7 @@ import { readFileSync, existsSync } from 'node:fs'; import { readStdinJson, getProjectDir, extractFilePath, isCrFile, isDevpaceFile -} from './lib/utils.mjs'; +} from '../lib/utils.mjs'; const input = await readStdinJson(); const projectDir = getProjectDir(); diff --git a/hooks/pace-init-scope-check.mjs b/hooks/skill/pace-init-scope-check.mjs similarity index 98% rename from hooks/pace-init-scope-check.mjs rename to hooks/skill/pace-init-scope-check.mjs index f4119bd..9bf00ad 100755 --- a/hooks/pace-init-scope-check.mjs +++ b/hooks/skill/pace-init-scope-check.mjs @@ -15,7 +15,7 @@ * 2 = block (target is outside allowed scope) */ -import { readStdinJson, getProjectDir, extractFilePath } from './lib/utils.mjs'; +import { readStdinJson, getProjectDir, extractFilePath } from '../lib/utils.mjs'; const input = await readStdinJson(); const projectDir = getProjectDir(); diff --git a/hooks/pace-review-scope-check.mjs b/hooks/skill/pace-review-scope-check.mjs similarity index 98% rename from hooks/pace-review-scope-check.mjs rename to hooks/skill/pace-review-scope-check.mjs index 6639110..83e275a 100755 --- a/hooks/pace-review-scope-check.mjs +++ b/hooks/skill/pace-review-scope-check.mjs @@ -25,7 +25,7 @@ import { readStdinJson, getProjectDir, extractFilePath, isDevpaceFile -} from './lib/utils.mjs'; +} from '../lib/utils.mjs'; const input = await readStdinJson(); const projectDir = getProjectDir(); diff --git a/skills/pace-dev/SKILL.md b/skills/pace-dev/SKILL.md index a2f8f7b..3bd8350 100644 --- a/skills/pace-dev/SKILL.md +++ b/skills/pace-dev/SKILL.md @@ -11,7 +11,7 @@ hooks: tool_name: "Write|Edit" hooks: - type: command - command: "${CLAUDE_PLUGIN_ROOT}/hooks/pace-dev-scope-check.mjs" + command: "${CLAUDE_PLUGIN_ROOT}/hooks/skill/pace-dev-scope-check.mjs" timeout: 5 --- diff --git a/skills/pace-init/SKILL.md b/skills/pace-init/SKILL.md index 98243cf..5efaa5c 100644 --- a/skills/pace-init/SKILL.md +++ b/skills/pace-init/SKILL.md @@ -10,7 +10,7 @@ hooks: tool_name: "Write|Edit" hooks: - type: command - command: "${CLAUDE_PLUGIN_ROOT}/hooks/pace-init-scope-check.mjs" + command: "${CLAUDE_PLUGIN_ROOT}/hooks/skill/pace-init-scope-check.mjs" timeout: 5 --- diff --git a/skills/pace-review/SKILL.md b/skills/pace-review/SKILL.md index 572d209..322deac 100644 --- a/skills/pace-review/SKILL.md +++ b/skills/pace-review/SKILL.md @@ -11,7 +11,7 @@ hooks: tool_name: "Write|Edit" hooks: - type: command - command: "${CLAUDE_PLUGIN_ROOT}/hooks/pace-review-scope-check.mjs" + command: "${CLAUDE_PLUGIN_ROOT}/hooks/skill/pace-review-scope-check.mjs" timeout: 5 --- diff --git a/tests/hooks/test_pace_dev_scope_check.mjs b/tests/hooks/test_pace_dev_scope_check.mjs index cb514b3..a781d1f 100644 --- a/tests/hooks/test_pace_dev_scope_check.mjs +++ b/tests/hooks/test_pace_dev_scope_check.mjs @@ -1,5 +1,5 @@ /** - * Integration tests for hooks/pace-dev-scope-check.mjs + * Integration tests for hooks/skill/pace-dev-scope-check.mjs * Tests the hook by spawning it as a subprocess with simulated stdin JSON. * Run: node --test tests/hooks/test_pace_dev_scope_check.mjs */ @@ -14,7 +14,7 @@ import { dirname } from 'node:path'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); -const HOOK_SCRIPT = join(__dirname, '..', '..', 'hooks', 'pace-dev-scope-check.mjs'); +const HOOK_SCRIPT = join(__dirname, '..', '..', 'hooks', 'skill', 'pace-dev-scope-check.mjs'); // ── Test helpers ──────────────────────────────────────────────────── diff --git a/tests/static/test_hooks.py b/tests/static/test_hooks.py index e909535..9d5b77a 100644 --- a/tests/static/test_hooks.py +++ b/tests/static/test_hooks.py @@ -27,7 +27,9 @@ } EXPECTED_SCRIPTS_SH = ["session-start.sh", "session-stop.sh", "pre-compact.sh", "session-end.sh"] -EXPECTED_SCRIPTS_MJS = ["pre-tool-use.mjs", "post-cr-update.mjs", "intent-detect.mjs", "subagent-stop.mjs", "pulse-counter.mjs", "post-tool-failure.mjs", "sync-push.mjs", "pace-dev-scope-check.mjs"] +EXPECTED_SCRIPTS_MJS = ["pre-tool-use.mjs", "post-cr-update.mjs", "intent-detect.mjs", "subagent-stop.mjs", "pulse-counter.mjs", "post-tool-failure.mjs", "sync-push.mjs"] +SKILL_HOOKS_DIR = HOOKS_DIR / "skill" +EXPECTED_SKILL_SCRIPTS = ["pace-dev-scope-check.mjs", "pace-init-scope-check.mjs", "pace-review-scope-check.mjs"] EXPECTED_SCRIPTS = EXPECTED_SCRIPTS_SH + EXPECTED_SCRIPTS_MJS @@ -85,6 +87,39 @@ def test_tc_hk_05_scripts_have_shebang(self): no_shebang.append(script) assert not no_shebang, f"Scripts missing shebang: {no_shebang}" + def test_tc_hk_03b_skill_scripts_exist(self): + """TC-HK-03b: All expected skill-level hook scripts exist in hooks/skill/.""" + missing = [] + for script in EXPECTED_SKILL_SCRIPTS: + if not (SKILL_HOOKS_DIR / script).exists(): + missing.append(script) + assert not missing, f"Missing skill hook scripts: {missing}" + + def test_tc_hk_04b_skill_scripts_executable(self): + """TC-HK-04b: Skill hook scripts have execute permission.""" + not_executable = [] + for script in EXPECTED_SKILL_SCRIPTS: + path = SKILL_HOOKS_DIR / script + if path.exists(): + mode = path.stat().st_mode + if not (mode & stat.S_IXUSR): + not_executable.append(script) + assert not not_executable, ( + f"Skill scripts lack execute permission: {not_executable}. " + f"Fix with: chmod +x hooks/skill/