diff --git a/.claude/rules/decisions.md b/.claude/rules/decisions.md index 5efe661..81ae5fc 100644 --- a/.claude/rules/decisions.md +++ b/.claude/rules/decisions.md @@ -1,36 +1,48 @@ # Team Decisions -## Core Architecture -- [markdown-source-of-truth](.claude/decisions/markdown-source-of-truth.md) — Markdown files are source of truth. FTS5 index is derived — delete it and it rebuilds. -- [append-only-decisions](.claude/decisions/append-only-decisions.md) — Append-only is enforced by git. Edit in place, delete when obsolete, git log preserves history. -- [git-is-the-history-layer](.claude/decisions/git-is-the-history-layer.md) — Edit decisions in place, delete when obsolete. Git tracks evolution — no supersedes/withdrawn needed. -- [stdlib-only-in-src](.claude/decisions/stdlib-only-in-src.md) — Zero external dependencies in src/. Stdlib-only Python 3.11+. -- [pure-methods-on-store](.claude/decisions/pure-methods-on-store.md) — No side effects at import time on DecisionStore. -- [team-first-decisions](.claude/decisions/team-first-decisions.md) — Decisions write to .claude/decisions/ in repo. Memory is fallback for non-repo contexts only. - -## Policy Engine -- [policy-evaluation-order](.claude/decisions/policy-evaluation-order.md) — BLOCK → LIFECYCLE → CONTEXT → NUDGE. Block/reject fail-fast. Plugin always forces ok=True (advisory). -- [session-state-in-tmp](.claude/decisions/session-state-in-tmp.md) — Per-session state in /tmp with atomic O_CREAT|O_EXCL marker files. - -## Hook Dispatch -- [bash-dispatch-fast-paths](.claude/decisions/bash-dispatch-fast-paths.md) — dispatch.sh avoids spawning Python for no-op events via fast-paths. -- [dispatch-errors-never-break-claude](.claude/decisions/dispatch-errors-never-break-claude.md) — dispatch.sh traps ERR and exits 0. Plugin errors must never break Claude Code. - -## Search & Storage -- [fts5-search-pipeline](.claude/decisions/fts5-search-pipeline.md) — FTS5 MATCH with BM25, fallback to weighted keyword matching. -- [incremental-sync-over-full-rebuild](.claude/decisions/incremental-sync-over-full-rebuild.md) — FTS5 index uses incremental sync with per-file mtime, not full rebuild. -- [tag-summary-over-per-decision-listing](.claude/decisions/tag-summary-over-per-decision-listing.md) — Session-context injects tag counts and query hint instead of listing individual decisions. - -## Skills & UX -- [single-skill-entry-point](.claude/decisions/single-skill-entry-point.md) — One /decision skill handles capture, search, and manage via intent detection. -- [no-capture-confirmation](.claude/decisions/no-capture-confirmation.md) — Agents write decisions without confirmation. /decision undo is the safety net. -- [merge-tags-stats-into-list](.claude/decisions/merge-tags-stats-into-list.md) — Consolidated /decision:tags and /decision:stats into /decision:list as --tags and --stats flags. -- [search-skill-prefer-preseeded](.claude/decisions/search-skill-prefer-preseeded.md) — Search skill presents pre-seeded hook results first, falls back to Glob/Grep only if needed. - -## Nudges -- [capture-nudge-corroboration-requirement](.claude/decisions/capture-nudge-corroboration-requirement.md) — Capture-nudge requires trigger phrase + technical signal (or 2+ phrases) to reduce false positives. -- [query-preseed-hook-for-skill](.claude/decisions/query-preseed-hook-for-skill.md) — UserPromptSubmit hook pre-seeds Python query results before the skill runs Glob/Grep. -- [stop-hook-nudge-for-decision-capture](.claude/decisions/stop-hook-nudge-for-decision-capture.md) — Stop hook nudges agent to capture decisions before session ends. - -- [stale-path-all-decisions](.claude/decisions/stale-path-all-decisions.md) — Stale affects-path warnings fire for all decision writes, not just new ones. - +## Affects +- [stale-path-all-decisions](.claude/decisions/stale-path-all-decisions.md) — Stale affects-path warnings fire for all decision writes, not just new ones + +## Architecture +- [append-only-decisions](.claude/decisions/append-only-decisions.md) — Append-only is enforced by git, not file conventions. Edit in place, delete when obsolete, git log preserves history. +- [bash-dispatch-fast-paths](.claude/decisions/bash-dispatch-fast-paths.md) — dispatch.sh avoids spawning Python for no-op hook events via fast-paths +- [dispatch-errors-never-break-claude](.claude/decisions/dispatch-errors-never-break-claude.md) — dispatch.sh traps ERR and exits 0 — plugin errors must never break Claude Code +- [fts5-search-pipeline](.claude/decisions/fts5-search-pipeline.md) — Search uses FTS5 with BM25 ranking, falling back to weighted keyword matching +- [git-is-the-history-layer](.claude/decisions/git-is-the-history-layer.md) — Edit decisions in place, delete when obsolete. Git tracks the full evolution — no supersedes/withdrawn ceremony needed. +- [markdown-source-of-truth](.claude/decisions/markdown-source-of-truth.md) — Markdown files are the source of truth — FTS5 index is derived and disposable +- [no-capture-confirmation](.claude/decisions/no-capture-confirmation.md) — Agents write decisions automatically without confirmation prompts — /decision undo is the safety net +- [policy-evaluation-order](.claude/decisions/policy-evaluation-order.md) — Policies evaluate BLOCK → LIFECYCLE → CONTEXT → NUDGE with fail-fast on block/reject +- [pure-methods-on-store](.claude/decisions/pure-methods-on-store.md) — No side effects at import time on DecisionStore +- [session-state-in-tmp](.claude/decisions/session-state-in-tmp.md) — Per-session state in /tmp with atomic O_CREAT|O_EXCL marker files for once-per-session policies +- [single-skill-entry-point](.claude/decisions/single-skill-entry-point.md) — One /decision skill handles capture, search, and manage via intent detection +- [skill-hook-cli-boundary](.claude/decisions/skill-hook-cli-boundary.md) — Skill = intent + behavior guidance, hooks = correctness enforcement, CLI = data access + computation +- [stdlib-only-in-src](.claude/decisions/stdlib-only-in-src.md) — Zero external dependencies in src/ — stdlib-only Python 3.11+ +- [team-first-decisions](.claude/decisions/team-first-decisions.md) — Decisions write to .claude/decisions/ in repo by default. Memory is fallback for non-repo contexts only. + +## Hooks +- [capture-nudge-corroboration-requirement](.claude/decisions/capture-nudge-corroboration-requirement.md) — Capture-nudge requires trigger phrase + technical signal (or 2+ phrases) to reduce false positives + +## Plugin Architecture +- [query-preseed-hook-for-skill](.claude/decisions/query-preseed-hook-for-skill.md) — UserPromptSubmit hook pre-seeds Python query results before the query skill runs Glob/Grep +- [stop-hook-nudge-for-decision-capture](.claude/decisions/stop-hook-nudge-for-decision-capture.md) — Stop hook nudges agent to capture decisions before session ends +- [tag-summary-over-per-decision-listing](.claude/decisions/tag-summary-over-per-decision-listing.md) — Session-context injects tag counts and query hint instead of listing individual decisions + +## Search +- [incremental-sync-over-full-rebuild](.claude/decisions/incremental-sync-over-full-rebuild.md) — FTS5 index uses incremental sync with per-file mtime instead of full rebuild on every change +- [search-skill-prefer-preseeded](.claude/decisions/search-skill-prefer-preseeded.md) — Search skill presents pre-seeded hook results first, falls back to Glob/Grep only if needed + +## Skills +- [merge-tags-stats-into-list](.claude/decisions/merge-tags-stats-into-list.md) — Consolidated /decision:tags and /decision:stats into /decision:list as --tags and --stats flags + +## Superpowers +- [superpowers-design-first](.claude/decisions/superpowers-design-first.md) — Always brainstorm and validate design before writing any code +- [superpowers-detailed-plans](.claude/decisions/superpowers-detailed-plans.md) — Implementation plans must be complete with zero placeholders +- [superpowers-git-worktrees](.claude/decisions/superpowers-git-worktrees.md) — Use git worktrees for feature branch isolation with verified baselines +- [superpowers-spec-before-quality](.claude/decisions/superpowers-spec-before-quality.md) — Review spec compliance before code quality — wrong code polished is waste +- [superpowers-subagent-development](.claude/decisions/superpowers-subagent-development.md) — Fresh subagent per task with two-stage review (spec then quality) +- [superpowers-systematic-debugging](.claude/decisions/superpowers-systematic-debugging.md) — Root cause investigation before any fixes — no random changes +- [superpowers-tdd](.claude/decisions/superpowers-tdd.md) — No production code without a failing test first (RED-GREEN-REFACTOR) +- [superpowers-verify-before-complete](.claude/decisions/superpowers-verify-before-complete.md) — No completion claims without fresh verification evidence + +## Versioning +- [semver-versioning](.claude/decisions/semver-versioning.md) — Use semantic versioning (semver) for all releases diff --git a/CHANGELOG.md b/CHANGELOG.md index 82ab5b5..496e1fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ All notable changes to this project will be documented in this file. +## Unreleased + +### Removed +- **Superpowers autoseeding** — the SessionStart hook no longer creates 8 `superpowers-*.md` decision files when the Superpowers plugin is detected. Manufactured decisions conflict with the zero-config, user-authored philosophy. Spec/plan extraction from `docs/superpowers/` files is unchanged. + ## 1.3.0 — 2026-04-13 ### Features diff --git a/README.md b/README.md index bb5efb7..966219d 100644 --- a/README.md +++ b/README.md @@ -97,9 +97,7 @@ See the [Guide](GUIDE.md) for all commands, `affects` matching rules, team adopt ## Superpowers integration -If you use [Superpowers](https://github.com/obra/superpowers), Code Decisions detects it automatically and does two things: - -**Seeds methodology decisions.** On first session, the plugin creates 8 decision files documenting the Superpowers methodology — TDD, design-before-code, systematic debugging, subagent-driven development, and more. These are searchable, tagged `superpowers`, and committed to git so your whole team inherits them. +If you use [Superpowers](https://github.com/obra/superpowers), Code Decisions detects it automatically and extracts decisions from your design specs and implementation plans. **Extracts decisions from specs and plans.** When Superpowers writes a design spec (`docs/superpowers/specs/`) or implementation plan (`docs/superpowers/plans/`), Code Decisions scans the content for trade-offs and approach choices. On the first implementation edit, it nudges the agent to capture those choices as decisions — so they surface automatically when teammates edit the affected files later. diff --git a/src/decision/policy/session_init.py b/src/decision/policy/session_init.py index 8a3d06a..41f53bd 100644 --- a/src/decision/policy/session_init.py +++ b/src/decision/policy/session_init.py @@ -47,16 +47,6 @@ def _session_init_condition(data: dict[str, Any], state: SessionState) -> Policy store = state.get_store() store.ensure_dir() - # Seed decisions from detected plugins (idempotent, fast) - try: - from ..seeds import get_registry - - seeded = get_registry().seed_decisions(store) - if seeded: - print(f" ◆ seeded {seeded} decisions from superpowers methodology", file=sys.stderr) - except Exception as exc: - print(f"decision: seed error: {exc}", file=sys.stderr) - # Rebuild rules index if decision files changed outside Claude Code _rebuild_index_if_stale(state) diff --git a/src/decision/seeds/__init__.py b/src/decision/seeds/__init__.py deleted file mode 100644 index 5471c7b..0000000 --- a/src/decision/seeds/__init__.py +++ /dev/null @@ -1,136 +0,0 @@ -"""Plugin-aware decision seeding — detect installed plugins, seed methodology decisions.""" - -from __future__ import annotations - -import dataclasses -import json -from datetime import date -from pathlib import Path -from typing import Any, Callable - -from ..store.store import DecisionStore -from ..utils.frontmatter import _format_yaml_frontmatter - -MANIFEST_FILE = ".seeded.json" -PLUGINS_JSON = Path.home() / ".claude" / "plugins" / "installed_plugins.json" - - -@dataclasses.dataclass -class SeedDecision: - """Template for a decision to seed from a detected plugin.""" - - slug: str - name: str - description: str - tags: list[str] - affects: list[str] - title: str - body: str - - -class SeedRegistry: - """Registry of decision seeds keyed by plugin identity.""" - - def __init__(self) -> None: - self._seeds: dict[str, Callable[[], list[SeedDecision]]] = {} - - def register(self, plugin_pattern: str, factory: Callable[[], list[SeedDecision]]) -> None: - """Register a seed factory for a plugin name pattern.""" - self._seeds[plugin_pattern] = factory - - def detect_installed_plugins(self) -> set[str]: - """Read installed_plugins.json and return matched plugin patterns.""" - try: - data = json.loads(PLUGINS_JSON.read_text()) - except (OSError, json.JSONDecodeError): - return set() - - plugin_keys = list(data.get("plugins", {}).keys()) - matched: set[str] = set() - for pattern in self._seeds: - for key in plugin_keys: - if pattern in key: - matched.add(pattern) - break - return matched - - def seed_decisions(self, store: DecisionStore) -> int: - """Seed decisions for detected plugins. Returns count of newly created files.""" - detected = self.detect_installed_plugins() - if not detected: - return 0 - - manifest = _load_manifest(store.decisions_dir) - count = 0 - today = date.today().isoformat() - - for pattern in detected: - factory = self._seeds[pattern] - seeds = factory() - already_seeded = set(manifest.get("seeded", {}).get(pattern, {}).get("slugs", [])) - - new_slugs: list[str] = [] - for seed in seeds: - if seed.slug in already_seeded: - continue # previously seeded (possibly deleted by user) - - dest = store.decisions_dir / f"{seed.slug}.md" - if dest.exists(): - # File exists (maybe user-created) — record in manifest but don't overwrite - new_slugs.append(seed.slug) - continue - - frontmatter = _format_yaml_frontmatter( - { - "name": seed.name, - "description": seed.description, - "date": today, - "tags": seed.tags, - "affects": seed.affects, - } - ) - content = f"{frontmatter}\n\n# {seed.title}\n\n{seed.body}\n" - dest.write_text(content) - new_slugs.append(seed.slug) - count += 1 - - if new_slugs: - seeded_section = manifest.setdefault("seeded", {}) - entry = seeded_section.setdefault(pattern, {"slugs": [], "seeded_at": today}) - entry["slugs"] = sorted(set(entry["slugs"]) | set(new_slugs)) - - if count: - _save_manifest(store.decisions_dir, manifest) - - return count - - -def _load_manifest(decisions_dir: Path) -> dict[str, Any]: - """Load the seeding manifest from .seeded.json.""" - path = decisions_dir / MANIFEST_FILE - try: - result: dict[str, Any] = json.loads(path.read_text()) - return result - except (OSError, json.JSONDecodeError): - return {"version": 1, "seeded": {}} - - -def _save_manifest(decisions_dir: Path, manifest: dict[str, Any]) -> None: - """Save the seeding manifest to .seeded.json.""" - manifest["version"] = 1 - path = decisions_dir / MANIFEST_FILE - path.write_text(json.dumps(manifest, indent=2, sort_keys=False) + "\n") - - -_registry: SeedRegistry | None = None - - -def get_registry() -> SeedRegistry: - """Return the singleton seed registry, lazily populated.""" - global _registry - if _registry is None: - _registry = SeedRegistry() - from ._superpowers import superpowers_seeds - - _registry.register("superpowers", superpowers_seeds) - return _registry diff --git a/src/decision/seeds/_superpowers.py b/src/decision/seeds/_superpowers.py deleted file mode 100644 index b6c276d..0000000 --- a/src/decision/seeds/_superpowers.py +++ /dev/null @@ -1,159 +0,0 @@ -"""Seed decisions for the Superpowers plugin (obra/superpowers). - -Each decision captures a methodology choice that Superpowers enforces — -design-first, TDD, systematic debugging, etc. These get written to -.claude/decisions/ so the team's adoption of the methodology is explicit, -searchable, and surfaces in related-context when editing affected files. -""" - -from __future__ import annotations - -from . import SeedDecision - -_PREFIX = "superpowers" - - -def superpowers_seeds() -> list[SeedDecision]: - """Return seed decisions representing the Superpowers methodology.""" - return [ - SeedDecision( - slug=f"{_PREFIX}-design-first", - name=f"{_PREFIX}-design-first", - description="Always brainstorm and validate design before writing any code", - tags=[_PREFIX, "workflow"], - affects=["src/", "docs/"], - title="Design before implementation", - body=( - "No code touches disk until the design is written and approved, " - "because unexamined assumptions are the most expensive kind of rework. " - "Even tasks that seem simple get a written design — those are highest-risk " - "for hidden complexity.\n\n" - "The workflow: explore context, ask clarifying questions, propose 2-3 approaches " - "with trade-offs, write a spec to docs/superpowers/specs/, and get explicit " - "approval before any implementation skill can run." - ), - ), - SeedDecision( - slug=f"{_PREFIX}-tdd", - name=f"{_PREFIX}-tdd", - description="No production code without a failing test first (RED-GREEN-REFACTOR)", - tags=[_PREFIX, "testing"], - affects=["src/", "tests/"], - title="Test-driven development (RED-GREEN-REFACTOR)", - body=( - "Tests written after implementation prove nothing — they're biased by the code " - "that already exists. Instead of verifying behavior, they verify implementation. " - "The RED-GREEN-REFACTOR cycle chosen here catches edge cases during design rather " - "than in production.\n\n" - "The rule: write one minimal failing test (RED), watch it fail to prove it tests " - "the right thing, write the simplest code to pass (GREEN), then clean up (REFACTOR). " - "Code written before tests must be deleted and restarted. No exceptions for " - "'simple' code — simple code breaks, and the test takes 30 seconds to write." - ), - ), - SeedDecision( - slug=f"{_PREFIX}-detailed-plans", - name=f"{_PREFIX}-detailed-plans", - description="Implementation plans must be complete with zero placeholders", - tags=[_PREFIX, "workflow"], - affects=["src/", "docs/"], - title="Implementation plans with zero placeholders", - body=( - "Placeholders like TBD, TODO, and 'similar to Task N' delay the discovery of " - "ambiguity until coding, when it's most expensive to resolve. Plans are written " - "instead for an engineer with no codebase context, because that's effectively what " - "a subagent is.\n\n" - "Each task is 2-5 minutes of work with exact file paths, complete code, and " - "verification steps. RED, verify RED, GREEN, verify GREEN, and COMMIT are " - "separate steps. This granularity prevents multi-hour rabbit holes and makes " - "progress observable." - ), - ), - SeedDecision( - slug=f"{_PREFIX}-subagent-development", - name=f"{_PREFIX}-subagent-development", - description="Fresh subagent per task with two-stage review (spec then quality)", - tags=[_PREFIX, "workflow"], - affects=["src/", "tests/"], - title="Subagent-driven development with two-stage review", - body=( - "A fresh subagent per task prevents context pollution — accumulated state from " - "earlier tasks biases later decisions. Instead of one long session that drifts, " - "each task gets clean context with the full task specification.\n\n" - "Review is two-stage in strict order: spec compliance first (does it match the " - "spec — no more, no less?), then code quality (is it well-written?). This order " - "matters because polishing wrong code is pure waste. Critical issues from review " - "block progress." - ), - ), - SeedDecision( - slug=f"{_PREFIX}-systematic-debugging", - name=f"{_PREFIX}-systematic-debugging", - description="Root cause investigation before any fixes — no random changes", - tags=[_PREFIX, "debugging"], - affects=["src/", "tests/"], - title="Root cause investigation before fixes", - body=( - "Random fixes waste time and create new bugs. The alternative chosen here is " - "systematic debugging: read error messages completely, reproduce consistently, " - "check recent changes, trace data flow backward to find where the bad value " - "originates.\n\n" - "Fixes require a stated hypothesis ('X causes Y because Z') tested one variable " - "at a time. If 3+ fixes have failed, stop — the pattern indicates an architectural " - "problem, not a coding problem. Discuss refactoring instead of attempting another " - "patch." - ), - ), - SeedDecision( - slug=f"{_PREFIX}-git-worktrees", - name=f"{_PREFIX}-git-worktrees", - description="Use git worktrees for feature branch isolation with verified baselines", - tags=[_PREFIX, "git"], - affects=["src/", "tests/"], - title="Git worktrees for feature isolation", - body=( - "Git worktrees provide true filesystem isolation for feature branches instead of " - "stashing or switching branches in-place, which risks contaminating the working " - "directory. This was chosen over simple branching because subagent-driven development " - "benefits from a clean, independent working copy.\n\n" - "The worktree directory must be gitignored (verified before creation). After checkout, " - "project setup runs automatically and tests are verified to pass at baseline. A failing " - "baseline is a hard stop — you can't distinguish new bugs from pre-existing ones." - ), - ), - SeedDecision( - slug=f"{_PREFIX}-verify-before-complete", - name=f"{_PREFIX}-verify-before-complete", - description="No completion claims without fresh verification evidence", - tags=[_PREFIX, "quality"], - affects=["src/", "tests/"], - title="Verification evidence before completion claims", - body=( - "Confidence is subjective, evidence is objective. Claiming 'tests pass' without " - "running the test suite, or 'build succeeds' without checking the exit code, is " - "not verification — it's guessing. This rule exists because unverified claims erode " - "trust and hide real failures.\n\n" - "Before any completion claim: identify the verification command, run it fresh, " - "read the full output, and confirm the output matches the claim. Partial checks " - "(linter passed but compiler not run) prove nothing. Words like 'should' and " - "'probably' are red flags that evidence is missing." - ), - ), - SeedDecision( - slug=f"{_PREFIX}-spec-before-quality", - name=f"{_PREFIX}-spec-before-quality", - description="Review spec compliance before code quality — wrong code polished is waste", - tags=[_PREFIX, "quality"], - affects=["src/", "tests/"], - title="Spec compliance review before code quality review", - body=( - "Code quality review before spec compliance review risks polishing the wrong " - "implementation — optimizing code that doesn't match the spec is pure waste. " - "By checking spec compliance first, wrong implementations are caught before any " - "effort goes into making them elegant.\n\n" - "The spec compliance check asks: does the code do exactly what was specified, " - "no more, no less? Over-building is as much a spec violation as under-building. " - "Only after spec compliance passes does code quality review begin." - ), - ), - ] diff --git a/tests/conftest.py b/tests/conftest.py index 8f529fa..d5957ee 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -23,17 +23,6 @@ def _isolate_state_dir(tmp_path): yield -@pytest.fixture(autouse=True) -def _isolate_seed_registry(tmp_path): - """Point PLUGINS_JSON to a nonexistent file so seeder is a no-op in all tests.""" - import decision.seeds as seeds_mod - - seeds_mod._registry = None # reset singleton so registrations don't leak - with patch.object(seeds_mod, "PLUGINS_JSON", tmp_path / "no-plugins.json"): - yield - seeds_mod._registry = None - - @pytest.fixture def dispatch_env(): """Env dict for subprocess hook tests.""" diff --git a/tests/test_seeds.py b/tests/test_seeds.py deleted file mode 100644 index f50d7f9..0000000 --- a/tests/test_seeds.py +++ /dev/null @@ -1,232 +0,0 @@ -"""Tests for plugin-aware decision seeding.""" - -import json -from pathlib import Path -from unittest.mock import patch - -from conftest import make_store - -from decision.core.decision import Decision -from decision.seeds import MANIFEST_FILE, SeedDecision, SeedRegistry, _load_manifest - - -# ── Seed validation ────────────────────────────────────────────────── - - -def test_superpowers_seeds_pass_validation(): - """Every superpowers seed decision must pass Decision.validate().""" - from decision.seeds._superpowers import superpowers_seeds - - for seed in superpowers_seeds(): - frontmatter = ( - f"---\n" - f'name: "{seed.name}"\n' - f'description: "{seed.description}"\n' - f'date: "2026-04-13"\n' - f"tags:\n" - ) - for tag in seed.tags: - frontmatter += f' - "{tag}"\n' - if seed.affects: - frontmatter += "affects:\n" - for a in seed.affects: - frontmatter += f' - "{a}"\n' - frontmatter += "---\n" - text = f"{frontmatter}\n# {seed.title}\n\n{seed.body}\n" - d = Decision.from_text(text) - errors = d.validate() - assert errors == [], f"Seed {seed.slug} failed validation: {errors}" - - -def test_superpowers_seeds_have_unique_slugs(): - """All slugs must be unique.""" - from decision.seeds._superpowers import superpowers_seeds - - slugs = [s.slug for s in superpowers_seeds()] - assert len(slugs) == len(set(slugs)) - - -def test_superpowers_seeds_prefixed(): - """All slugs should be prefixed with 'superpowers-'.""" - from decision.seeds._superpowers import superpowers_seeds - - for seed in superpowers_seeds(): - assert seed.slug.startswith("superpowers-"), f"{seed.slug} missing prefix" - - -# ── Detection ──────────────────────────────────────────────────────── - - -def _make_plugins_json(tmp_path, plugin_keys): - """Create a mock installed_plugins.json.""" - plugins = {} - for key in plugin_keys: - plugins[key] = [{"scope": "user", "installPath": str(tmp_path / "cache" / key)}] - data = {"version": 2, "plugins": plugins} - path = tmp_path / "installed_plugins.json" - path.write_text(json.dumps(data)) - return path - - -def test_detect_superpowers_installed(tmp_path): - """Registry detects superpowers when present in installed_plugins.json.""" - plugins_json = _make_plugins_json(tmp_path, ["superpowers@superpowers-marketplace"]) - registry = SeedRegistry() - registry.register("superpowers", lambda: []) - - with patch("decision.seeds.PLUGINS_JSON", plugins_json): - detected = registry.detect_installed_plugins() - - assert "superpowers" in detected - - -def test_detect_superpowers_official_marketplace(tmp_path): - """Registry detects superpowers from the official marketplace key.""" - plugins_json = _make_plugins_json(tmp_path, ["superpowers@claude-plugins-official"]) - registry = SeedRegistry() - registry.register("superpowers", lambda: []) - - with patch("decision.seeds.PLUGINS_JSON", plugins_json): - detected = registry.detect_installed_plugins() - - assert "superpowers" in detected - - -def test_detect_no_superpowers(tmp_path): - """Registry returns empty when superpowers is not installed.""" - plugins_json = _make_plugins_json(tmp_path, ["other-plugin@marketplace"]) - registry = SeedRegistry() - registry.register("superpowers", lambda: []) - - with patch("decision.seeds.PLUGINS_JSON", plugins_json): - detected = registry.detect_installed_plugins() - - assert detected == set() - - -def test_detect_missing_plugins_json(tmp_path): - """Missing installed_plugins.json returns empty set.""" - registry = SeedRegistry() - registry.register("superpowers", lambda: []) - - with patch("decision.seeds.PLUGINS_JSON", tmp_path / "nonexistent.json"): - detected = registry.detect_installed_plugins() - - assert detected == set() - - -def test_detect_corrupt_plugins_json(tmp_path): - """Corrupt installed_plugins.json returns empty set.""" - bad_json = tmp_path / "installed_plugins.json" - bad_json.write_text("not json {{{") - registry = SeedRegistry() - registry.register("superpowers", lambda: []) - - with patch("decision.seeds.PLUGINS_JSON", bad_json): - detected = registry.detect_installed_plugins() - - assert detected == set() - - -# ── Seeding ────────────────────────────────────────────────────────── - - -def _make_registry_with_seeds(plugins_json): - """Create a registry with superpowers seeds and mocked detection.""" - from decision.seeds._superpowers import superpowers_seeds - - registry = SeedRegistry() - registry.register("superpowers", superpowers_seeds) - return registry - - -def test_seed_creates_files(tmp_path): - """seed_decisions creates decision files when superpowers is detected.""" - plugins_json = _make_plugins_json(tmp_path, ["superpowers@marketplace"]) - _, store = make_store(tmp_path) - - registry = _make_registry_with_seeds(plugins_json) - with patch("decision.seeds.PLUGINS_JSON", plugins_json): - count = registry.seed_decisions(store) - - assert count == 8 - assert (store.decisions_dir / "superpowers-tdd.md").is_file() - assert (store.decisions_dir / "superpowers-design-first.md").is_file() - - -def test_seed_idempotent(tmp_path): - """Second call to seed_decisions creates 0 new files.""" - plugins_json = _make_plugins_json(tmp_path, ["superpowers@marketplace"]) - _, store = make_store(tmp_path) - - registry = _make_registry_with_seeds(plugins_json) - with patch("decision.seeds.PLUGINS_JSON", plugins_json): - first = registry.seed_decisions(store) - second = registry.seed_decisions(store) - - assert first == 8 - assert second == 0 - - -def test_seed_respects_deletion(tmp_path): - """Deleted decision file is not re-created on subsequent seed.""" - plugins_json = _make_plugins_json(tmp_path, ["superpowers@marketplace"]) - _, store = make_store(tmp_path) - - registry = _make_registry_with_seeds(plugins_json) - with patch("decision.seeds.PLUGINS_JSON", plugins_json): - registry.seed_decisions(store) - - # Delete one decision - tdd_file = store.decisions_dir / "superpowers-tdd.md" - assert tdd_file.exists() - tdd_file.unlink() - - # Seed again — should not recreate - with patch("decision.seeds.PLUGINS_JSON", plugins_json): - count = registry.seed_decisions(store) - - assert count == 0 - assert not tdd_file.exists() - - -def test_seed_writes_manifest(tmp_path): - """Seeding creates a .seeded.json manifest.""" - plugins_json = _make_plugins_json(tmp_path, ["superpowers@marketplace"]) - _, store = make_store(tmp_path) - - registry = _make_registry_with_seeds(plugins_json) - with patch("decision.seeds.PLUGINS_JSON", plugins_json): - registry.seed_decisions(store) - - manifest = _load_manifest(store.decisions_dir) - assert "superpowers" in manifest["seeded"] - assert len(manifest["seeded"]["superpowers"]["slugs"]) == 8 - - -def test_seed_noop_when_not_installed(tmp_path): - """No files created when superpowers is not installed.""" - plugins_json = _make_plugins_json(tmp_path, ["other-plugin@marketplace"]) - _, store = make_store(tmp_path) - - registry = _make_registry_with_seeds(plugins_json) - with patch("decision.seeds.PLUGINS_JSON", plugins_json): - count = registry.seed_decisions(store) - - assert count == 0 - assert not (store.decisions_dir / MANIFEST_FILE).exists() - - -def test_seeded_files_are_valid_decisions(tmp_path): - """Every file created by seeding must parse and validate as a Decision.""" - plugins_json = _make_plugins_json(tmp_path, ["superpowers@marketplace"]) - _, store = make_store(tmp_path) - - registry = _make_registry_with_seeds(plugins_json) - with patch("decision.seeds.PLUGINS_JSON", plugins_json): - registry.seed_decisions(store) - - for md_file in store.decisions_dir.glob("superpowers-*.md"): - d = Decision.from_file(md_file) - errors = d.validate() - assert errors == [], f"{md_file.name} failed validation: {errors}"