Spec-driven development on OpenSpec, with mechanical spec-as-source enforcement: a custom 'spec-as-source' OpenSpec schema adds file-ownership (targets) and test-verification ([@test]) metadata to every capability spec, three scripts (link check, ownership check, manifest build) keep code and specs from drifting apart, plus requirement-gathering, spec-writer, work-review, and a session-handoff skill with a proactive context-warning hook.
68
85%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Low
Low-risk findings worth noting
#!/usr/bin/env python3
# GENERATED FROM SPEC — DO NOT EDIT DIRECTLY
# Source: openspec/specs/plan-mode/spec.md
"""Inspect optional product-context sources without mutating the project."""
from __future__ import annotations
import argparse
import hashlib
import json
import os
import shlex
import sys
from pathlib import Path
from typing import Any, Iterable, Mapping
try:
import yaml
except ImportError: # Reported as a named contract failure only when needed.
yaml = None # type: ignore[assignment]
CONTRACT_VERSION = 1
SOURCE_TYPES = ("prd", "personas", "tech-stack", "adr", "backlog")
CONVENTIONAL = {
"prd": ("PRD.md", "docs/PRD.md"),
"personas": ("USER-PERSONAS.md", "docs/USER-PERSONAS.md"),
"tech-stack": ("TECH-STACK.md", "docs/TECH-STACK.md"),
"backlog": ("BACKLOG.md", "docs/BACKLOG.md"),
}
ADR_EXACT = ("ADR.md", "docs/ADR.md")
ADR_GLOBS = ("adr/*.md", "docs/adr/*.md")
class InvalidProductContext(ValueError):
"""The explicit manifest cannot be interpreted safely."""
def sha256(path: Path) -> str:
return hashlib.sha256(path.read_bytes()).hexdigest()
def verification_command(relative: str) -> str:
code = (
"import hashlib,pathlib; "
f"p=pathlib.Path({relative!r}); "
"print(hashlib.sha256(p.read_bytes()).hexdigest())"
)
return f"python3 -c {shlex.quote(code)}"
def within_project(project_root: Path, resolved: Path) -> bool:
try:
resolved.relative_to(project_root)
except ValueError:
return False
return True
def normalize_manifest_path(project_root: Path, raw: str) -> tuple[str, Path]:
if not raw or "\x00" in raw:
raise InvalidProductContext("source paths must be non-empty strings")
candidate = Path(raw)
if candidate.is_absolute():
raise InvalidProductContext(f"absolute source path is forbidden: {raw}")
normalized = Path(os.path.normpath(raw))
if normalized == Path("."):
raise InvalidProductContext(f"source path must name a regular file: {raw}")
configured = project_root / normalized
try:
resolved = configured.resolve(strict=False)
except (OSError, RuntimeError) as exc:
raise InvalidProductContext(f"cannot resolve source path {raw}: {exc}") from exc
if not within_project(project_root, resolved):
raise InvalidProductContext(f"source path escapes the project: {raw}")
if configured.exists() and not configured.is_file():
raise InvalidProductContext(f"source path is not a regular file: {raw}")
relative = normalized.as_posix()
return relative, configured
def file_item(relative: str, path: Path) -> dict[str, Any]:
if not path.exists():
return {
"path": relative,
"state": "missing",
"sha256": None,
"verify_command": None,
}
digest = sha256(path)
return {
"path": relative,
"state": "fresh",
"sha256": digest,
"verify_command": verification_command(relative),
}
def source_bucket(state: str, files: Iterable[dict[str, Any]] = ()) -> dict[str, Any]:
ordered = sorted(files, key=lambda item: item["path"])
return {"state": state, "files": ordered}
def load_recorded_digests(path: Path) -> dict[tuple[str, str], str]:
try:
document = json.loads(path.read_text(encoding="utf-8"))
except (OSError, UnicodeError, json.JSONDecodeError) as exc:
raise InvalidProductContext(f"cannot read recorded inventory {path}: {exc}") from exc
if not isinstance(document, dict) or document.get("version") != CONTRACT_VERSION:
raise InvalidProductContext("recorded inventory must be a version 1 inventory object")
sources = document.get("sources")
if not isinstance(sources, dict):
raise InvalidProductContext("recorded inventory sources must be a map")
digests: dict[tuple[str, str], str] = {}
for source_type in SOURCE_TYPES:
bucket = sources.get(source_type)
if not isinstance(bucket, dict) or not isinstance(bucket.get("files"), list):
raise InvalidProductContext(f"recorded inventory source {source_type} is malformed")
for item in bucket["files"]:
if not isinstance(item, dict) or not isinstance(item.get("path"), str):
raise InvalidProductContext(f"recorded inventory source {source_type} has a malformed file")
digest = item.get("sha256")
if digest is None:
continue
if not isinstance(digest, str) or len(digest) != 64:
raise InvalidProductContext(
f"recorded inventory source {source_type} has an invalid SHA-256"
)
try:
int(digest, 16)
except ValueError as exc:
raise InvalidProductContext(
f"recorded inventory source {source_type} has an invalid SHA-256"
) from exc
key = (source_type, item["path"])
if key in digests:
raise InvalidProductContext(
f"recorded inventory repeats {source_type} path {item['path']}"
)
digests[key] = digest.lower()
return digests
def apply_recorded_digests(
sources: dict[str, Any],
recorded: Mapping[tuple[str, str], str],
) -> None:
for source_type in SOURCE_TYPES:
bucket = sources[source_type]
for item in bucket["files"]:
digest = recorded.get((source_type, item["path"]))
if digest is None:
continue
item["recorded_sha256"] = digest
if item["state"] != "missing":
item["state"] = "fresh" if item["sha256"] == digest else "stale"
if bucket["state"] == "conflict":
continue
states = {item["state"] for item in bucket["files"]}
if "stale" in states:
bucket["state"] = "stale"
elif "missing" in states:
bucket["state"] = "missing"
elif bucket["files"]:
bucket["state"] = "fresh"
def load_manifest(project_root: Path, manifest: Path) -> dict[str, Any]:
if yaml is None:
raise InvalidProductContext("PyYAML is required to read openspec/product-context.yaml")
try:
document = yaml.safe_load(manifest.read_text(encoding="utf-8"))
except (OSError, UnicodeError, yaml.YAMLError) as exc:
raise InvalidProductContext(f"cannot parse openspec/product-context.yaml: {exc}") from exc
if not isinstance(document, dict):
raise InvalidProductContext("manifest root must be a map")
if any(not isinstance(key, str) for key in document):
raise InvalidProductContext("manifest keys must be strings")
keys = set(document)
expected = {"version", "sources"}
if keys != expected:
missing = sorted(expected - keys)
unknown = sorted(keys - expected)
details = []
if missing:
details.append(f"missing keys: {', '.join(missing)}")
if unknown:
details.append(f"unknown keys: {', '.join(unknown)}")
raise InvalidProductContext("manifest schema mismatch (" + "; ".join(details) + ")")
version = document["version"]
if type(version) is not int or version != CONTRACT_VERSION:
raise InvalidProductContext(f"unsupported manifest version: {version!r}")
sources = document["sources"]
if not isinstance(sources, dict):
raise InvalidProductContext("sources must be a map")
if any(not isinstance(key, str) for key in sources):
raise InvalidProductContext("source type keys must be strings")
unknown_types = sorted(set(sources) - set(SOURCE_TYPES))
if unknown_types:
raise InvalidProductContext(f"unknown source types: {', '.join(unknown_types)}")
inventory: dict[str, Any] = {}
for source_type in SOURCE_TYPES:
if source_type not in sources:
inventory[source_type] = source_bucket("not configured")
continue
configured_paths = sources[source_type]
if not isinstance(configured_paths, list):
raise InvalidProductContext(f"source {source_type} must be a list")
if any(not isinstance(raw, str) for raw in configured_paths):
raise InvalidProductContext(f"source {source_type} entries must be strings")
items_by_path: dict[str, dict[str, Any]] = {}
for raw in configured_paths:
relative, path = normalize_manifest_path(project_root, raw)
items_by_path[relative] = file_item(relative, path)
items = list(items_by_path.values())
if not items:
state = "not configured"
elif any(item["state"] == "missing" for item in items):
state = "missing"
else:
state = "fresh"
inventory[source_type] = source_bucket(state, items)
return inventory
def safe_conventional_file(project_root: Path, relative: str) -> dict[str, Any] | None:
path = project_root / relative
if not path.is_file():
return None
try:
resolved = path.resolve(strict=True)
except (OSError, RuntimeError):
return None
if not within_project(project_root, resolved):
return None
return file_item(relative, path)
def discover_conventional(project_root: Path) -> dict[str, Any]:
inventory: dict[str, Any] = {}
for source_type in SOURCE_TYPES:
if source_type == "adr":
continue
items = [
item
for relative in CONVENTIONAL[source_type]
if (item := safe_conventional_file(project_root, relative)) is not None
]
if len(items) > 1:
state = "conflict"
elif items:
state = "fresh"
else:
state = "not configured"
inventory[source_type] = source_bucket(state, items)
adr_paths = set(ADR_EXACT)
for pattern in ADR_GLOBS:
adr_paths.update(path.relative_to(project_root).as_posix() for path in project_root.glob(pattern))
adr_items = [
item
for relative in sorted(adr_paths)
if (item := safe_conventional_file(project_root, relative)) is not None
]
inventory["adr"] = source_bucket("fresh" if adr_items else "not configured", adr_items)
return {source_type: inventory[source_type] for source_type in SOURCE_TYPES}
def inspect(project_root: Path, recorded_inventory: Path | None = None) -> dict[str, Any]:
try:
root = project_root.resolve(strict=True)
except (OSError, RuntimeError) as exc:
raise InvalidProductContext(f"project root cannot be resolved: {project_root}: {exc}") from exc
if not root.is_dir():
raise InvalidProductContext(f"project root is not a directory: {project_root}")
manifest = root / "openspec/product-context.yaml"
if manifest.exists():
if not manifest.is_file():
raise InvalidProductContext("openspec/product-context.yaml is not a regular file")
mode = "manifest"
sources = load_manifest(root, manifest)
else:
mode = "conventional"
sources = discover_conventional(root)
if recorded_inventory is not None:
apply_recorded_digests(sources, load_recorded_digests(recorded_inventory))
return {"version": CONTRACT_VERSION, "mode": mode, "sources": sources}
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--project-root",
type=Path,
default=Path.cwd(),
help="project root to inspect (defaults to the current directory)",
)
parser.add_argument(
"--recorded-inventory",
type=Path,
help="previous helper JSON whose SHA-256 values are the freshness baseline",
)
args = parser.parse_args(argv)
try:
result = inspect(args.project_root, args.recorded_inventory)
except (InvalidProductContext, OSError) as exc:
print(f"INVALID-PRODUCT-CONTEXT: {exc}", file=sys.stderr)
return 2
print(json.dumps(result, indent=2, ensure_ascii=False))
return 0
if __name__ == "__main__":
raise SystemExit(main()).tessl-plugin
rules
skills
handoff
handoff-skill
openspec-apply-change
openspec-archive-change
openspec-explore
openspec-propose
openspec-sync-specs
plan-judge
plan-mode
prompt-loop
requirement-gathering
spec-as-source-setup
templates
openspec-schema
spec-as-source
templates
spec-ci-sync
spec-loop
spec-rebuild
spec-verify
spec-writer
work-review