This page explains PactKit's internal architecture. It's useful for contributors, advanced users, and anyone who wants to understand how the toolkit is built.
Design Principles
PactKit follows these architecture principles (codified in 08-architecture-principles.md):
- Open-Closed Principle (OCP) — Add new formats/rules/agents by adding entries to registries, not modifying core logic
- Single Source of Truth — Each data type has exactly one canonical location
- DRY — Shared logic lives in one place; standalone scripts use inline copies with source-of-truth comments
- Prompt-First — Behavioral changes are prompt modifications (cheapest, highest impact) before code changes
- Conditional by Default — All optional features (CI, hooks, MCP) are off by default
Module Overview
src/pactkit/
├── cli.py ← CLI entry point: init/update/version/upgrade/schema/spec-lint
├── config.py ← Config load/validate/generate + re-exports from profiles
├── profiles.py ← FormatProfile frozen dataclass registry (OCP)
├── schemas.py ← Document structure schemas (Spec, Board, Context, Lessons)
├── utils.py ← Shared utilities (atomic_write)
├── generators/
│ └── deployer.py ← Core deployment orchestrator
├── prompts/ ← All prompt templates and constants
│ ├── agents.py ← 9 agent role definitions
│ ├── commands.py ← 11 command playbooks (with {TEMPLATE_VAR} placeholders)
│ ├── references.py ← Reference checklists (SOLID/Security/Quality)
│ ├── rules.py ← 8 rule modules + CLAUDE_MD_TEMPLATE
│ ├── skills.py ← 10 skill definitions
│ └── workflows.py ← PDCA workflow prompts + LANG_PROFILES
└── skills/ ← Standalone skill scripts
├── board.py ← Sprint Board operations
├── scaffold.py ← File scaffolding
├── spec_linter.py ← Non-AI Spec structural validation
└── visualize.py ← Code dependency graph (Mermaid)Key Design Decisions
FormatProfile Registry
Adding a new AI tool format (e.g., Cursor, Copilot) requires only one entry in profiles.py:
@dataclass(frozen=True)
class FormatProfile:
name: str # "opencode"
config_dir: str # ".opencode"
skills_path_var: str # "~/.config/opencode/skills"
instructions_file: str # "AGENTS.md"
excluded_agent_fields: tuple # Fields to remove from agent definitions
# ... more fieldsFunctions read profile.skills_path_var instead of checking if format == "opencode".
Template Variable System
Prompt templates use {PLACEHOLDER} syntax resolved at deploy time:
# In commands.py (template):
"Run: python3 {SKILLS_ROOT}/{name}/scripts/board.py add_story ..."
# In deployer.py (resolution):
def _render_prompt(template: str, profile: FormatProfile) -> str:
result = template
result = result.replace("{SKILLS_ROOT}", profile.skills_path_var)
result = result.replace("{BOARD_CMD}", profile.board_cmd)
# ... 11 variables total
return resultSequential str.replace() is used instead of str.format_map() because user-facing templates contain patterns like {R1, R2} and {version}_*.mmd that cause ValueError in Python's format parser.
Document Schema Registry
schemas.py is the single source of truth for document structure:
SCHEMA_REGISTRY = {
"spec": SpecSchema, # Required sections, metadata fields, AC format
"board": BoardSchema, # Section headers, story format, task format
"context": ContextSchema, # Section list, timestamp format
"lessons": LessonsSchema, # Table format, column definitions
"test_case": TestCaseSchema, # Gherkin format requirements
}The Spec linter (spec_linter.py) and scaffold (scaffold.py) both reference these schemas. Since standalone scripts can't import library modules, they use inline copies with a comment pointing to the canonical source.
Lazy Rule Loading
Rules are split into two tiers for OpenCode:
Core (always loaded via opencode.json instructions):
01-core-protocol.md ← Session context, TDD, visual-first
02-hierarchy-of-truth.md ← Spec > Tests > Code
09-credential-safety.md ← Credential protection (user-managed)
On-demand (loaded by AI via @reference in AGENTS.md):
03-file-atlas.md ← Standard file locations
04-routing-table.md ← Command → agent mapping
05-workflow-conventions.md ← Commit, branch, PR conventions
06-mcp-integration.md ← MCP server usage by phase
07-shared-protocols.md ← Lazy Visualize, Test Mapping
08-architecture-principles.md ← SOLID/DRY patternsThe AI reads on-demand rules only when the current task requires them. This reduces per-turn overhead by ~62%.
Claude Code uses @import which loads all rules every turn — no lazy loading optimization is possible there.
Deployment Pipeline
The deployer (generators/deployer.py) handles four formats:
| Format | Target | Use Case |
|---|---|---|
classic | ~/.claude/ | Claude Code native |
opencode | ~/.config/opencode/ | OpenCode native |
plugin | Plugin cache directory | Claude Code marketplace |
marketplace | Self-contained archive | Distribution package |
Each format shares the same prompt templates but applies format-specific transformations:
- Path rewriting (
~/.claude/skills/→~/.config/opencode/skills/) - Agent field removal (Claude-only fields stripped for OpenCode)
- Frontmatter injection (
model:routing for OpenCode commands) - Config file generation (
opencode.jsonvsCLAUDE.md)
Safe Regression
PactKit prevents agents from breaking pre-existing tests through a layered approach:
- TDD Loop — Only iterates on tests created in the current Story
- Pre-existing Test Protection — If a test that existed before the current Story fails, the agent STOPs and reports (never modifies)
- Done Regression Gate — Full test suite must pass before committing
- Incremental Mode — Only used when ALL safety conditions are met (same Story, no base branch divergence)
Data Flow
User requirement
→ /project-plan → System Architect → Spec + Board entry
→ /project-act → Senior Developer → Tests + Implementation
→ /project-check → QA Engineer → Test cases + Verdict
→ /project-done → Repo Maintainer → Commit + Archive + Context updateCross-session state is maintained through:
docs/product/context.md— auto-generated sprint status snapshotdocs/architecture/governance/lessons.md— append-only knowledge basedocs/product/sprint_board.md— current work items
Contributing
To add a new component:
| Component | Files to Touch |
|---|---|
| New agent | agents.py + config.py VALID_AGENTS |
| New command | commands.py + config.py VALID_COMMANDS + rules.py routing table |
| New skill | skills.py + config.py VALID_SKILLS |
| New rule | rules.py RULES_FILES + config.py VALID_RULES |
| New format | profiles.py FORMAT_PROFILES (one entry) |
| New config section | 7 touch points in config.py (see lessons.md) |