Skip to content

Latest commit

 

History

History
539 lines (392 loc) · 16.5 KB

File metadata and controls

539 lines (392 loc) · 16.5 KB

Fork Maintenance Guide

This document describes how the tikalk fork (tikalk/agentic-sdlc-spec-kit) maintains customizations while staying synced with upstream github/spec-kit.

Philosophy

The fork isolates all customizations into a single file (cli_customization.py) to minimize merge conflicts. When syncing with upstream:

  1. Only the import block in __init__.py may conflict
  2. All customization logic lives in cli_customization.py
  3. Upstream changes to other parts of __init__.py merge cleanly

Fork Versioning Scheme

The fork uses a suffix-based versioning system to track both upstream and fork-specific changes:

Version Format

<upstream-version>+adlc<N>

Examples:

  • 0.8.2+adlc1 - Fork based on upstream 0.8.2, first fork release
  • 0.8.2+adlc2 - Same upstream base, second fork release with new features
  • 0.9.0+adlc1 - After upstream merge to 0.9.0, reset fork counter

When to Bump

Scenario Version Change Example
Fork adds new feature Increment adlc suffix 0.8.2+adlc10.8.2+adlc2
Merge upstream release Update base, reset suffix 0.8.2+adlc50.9.0+adlc1
Hotfix/patch Increment adlc suffix 0.8.2+adlc10.8.2+adlc2

Tag Format

Use agentic-sdlc-v<version> with dashes instead of plus:

  • Version: 0.8.2+adlc2
  • Tag: agentic-sdlc-v0.8.2-adlc2

Customization Module

File: src/specify_cli/cli_customization.py

This module provides:

Feature Upstream Default Fork Override
ACCENT_COLOR "cyan" "#f47721" (tikalk orange)
BANNER_COLORS cyan gradient orange gradient
accent() N/A Helper function for theming
accent_style() N/A Helper for Rich style= params
PKG_NAMES ("specify-cli",) ("agentic-sdlc-specify-cli", "specify-cli")
TEAM_DIRECTIVES_DIRNAME N/A "team-ai-directives"
EXTENSION_NAMESPACES ["speckit"] ["speckit", "adlc"]
EXTENSION_ALIAS_PATTERN_ENABLED False True

Import Block

The __init__.py file starts with this import block that you should preserve during merges:

# Tikalk fork customizations - import with fallback to upstream defaults
try:
    from .cli_customization import (
        ACCENT_COLOR,
        BANNER_COLORS,
        TAGLINE,
        accent,
        accent_style,
        TEAM_DIRECTIVES_DIRNAME,
        PKG_NAMES,
        pre_init,
        post_init,
        skill_app,
        compute_skill_output_name,
    )
except ImportError:
    ACCENT_COLOR = "cyan"
    BANNER_COLORS = ["#00ffff", "#00cccc", "cyan", "#009999", "white", "bright_white"]
    TEAM_DIRECTIVES_DIRNAME = "team-ai-directives"
    PKG_NAMES = ["specify-cli"]
    skill_app = None

    def accent(
        text: str, bold: bool = False, italic: bool = False, dim: bool = False
    ) -> str:
        style = ACCENT_COLOR
        if bold:
            style = f"bold {style}"
        if italic:
            style = f"italic {style}"
        if dim:
            style = f"dim {style}"
        return f"[{style}]{text}[/]"

    def accent_style() -> str:
        return ACCENT_COLOR

    def pre_init(project_path, selected_ai, team_ai_directives, tracker=None):
        pass

    def post_init(project_path, selected_ai, tracker=None, no_git=False):
        pass

    def compute_skill_output_name():
        return None

Critical: This block must include ALL fork customizations with fallbacks, not just partial imports. The import block is the SINGLE SOURCE OF TRUTH for fork customizations - all theming, hooks (pre_init, post_init), and helper functions must be defined here.

Extension Namespace Configuration

In extensions.py, the fork configures command name patterns:

# Get namespaces from customization module (supports speckit and adlc)
try:
    from .cli_customization import EXTENSION_NAMESPACES, EXTENSION_ALIAS_PATTERN_ENABLED
except ImportError:
    EXTENSION_NAMESPACES = ["speckit"]
    EXTENSION_ALIAS_PATTERN_ENABLED = False

EXTENSION_COMMAND_NAME_PATTERN = re.compile(
    rf"^(?:{'|' . join(EXTENSION_NAMESPACES)})\.([a-z0-9-]+)\.([a-z0-9-]+)$"
)

if EXTENSION_ALIAS_PATTERN_ENABLED:
    EXTENSION_ALIAS_NAME_PATTERN = re.compile(r"^([a-z0-9-]+)\.([a-z0-9-]+)$")
else:
    EXTENSION_ALIAS_NAME_PATTERN = None

Merging Upstream

This section documents the complete workflow for merging upstream changes from github/spec-kit.

Pre-Merge: Check for New Upstream Commits

# Fetch latest upstream changes
git fetch upstream

# Check what commits are new in upstream (not in origin)
git log origin/main --not upstream/main
git log upstream/main --not origin/main

Complete Merge Workflow

Step 1: Create Backup Branch

git checkout main
git branch backup-before-upstream-merge-$(date +%Y%m%d)

Step 2: Fetch and Merge

git fetch upstream
git merge upstream/main

Step 3: Resolve Conflicts

CRITICAL: Never use git checkout --theirs for init.py or pyproject.toml

Correct Strategy: Use upstream as clean base, then ADD fork customizations on top.

  1. For __init__.py: Merge upstream cleanly (no --theirs). Then manually add fork functions AFTER merge:

    • TEAM_DIRECTIVES_DIRNAME = "team-ai-directives"
    • _run_git_command()
    • sync_team_ai_directives()
    • compute_skill_output_name() (delegates to cli_customization)
    • TAGLINE (fork version)
  2. For pyproject.toml: NEVER use --theirs. The file will be in conflict - manually edit to preserve fork values:

    • name = "agentic-sdlc-specify-cli"
    • version = "0.5.X" (fork version)
    • description (fork description)
    • Wheel paths: update to root directories (see below)
    • Add httpx>=0.27.0 dependency
  3. For test files: Accept upstream versions to avoid massive diffs:

    git checkout --theirs tests/

Why this works:

  • No merge conflicts in init.py because we're using upstream as base
  • Fork functions are small additions (~50 lines) - easy to add from backup
  • pyproject.toml needs manual edit to preserve fork version (never use --theirs)

Step 4: Verify

python3 -m pytest tests/ -v

Step 5: Bump Version

Update pyproject.toml:

version = "0.3.X"  # Increment from current version

Step 6: Update CHANGELOG

Add new entry at top of changelog (after ## [Unreleased] or before latest release):

## [0.3.X] - YYYY-MM-DD

### Changed

- **Upstream merge**: Synced with github/spec-kit
  - [List upstream commits merged]

Step 7: Commit and Push

git add -A
git commit -m "chore: merge upstream and bump to v0.3.X"
git push origin main

Step 8: Create Tag

git tag -a agentic-sdlc-v0.3.X -m "Release agentic-sdlc-v0.3.X"
git push origin agentic-sdlc-v0.3.X

Conflict Resolution Strategy

When conflicts occur during merge:

  1. Keep origin changes as base - Our customizations in cli_customization.py and fork-specific features must be preserved
  2. Adapt upstream changes - Integrate upstream improvements to work with our customizations
  3. Test after resolving - Always run tests before committing

Common Conflict Points

File Why It Conflicts Resolution
__init__.py Import block at top Use our try/except import pattern with fallback
pyproject.toml Version number Increment version after merge
CHANGELOG.md New entries Add fork-specific entries, preserve upstream section

What NOT to Change During Merge

These fork customizations should NEVER be modified unless intentionally updating them:

  • cli_customization.py - All theming and customization constants
  • extensions.py - Extension namespace configuration
  • Bundled extensions in pyproject.toml - levelup, architect, quick, product, tdd
  • Bundled presets in pyproject.toml - agentic-sdlc

What Stays in cli_customization.py

The following customization categories must stay in cli_customization.py:

  1. Theming: ACCENT_COLOR, BANNER_COLORS, accent(), accent_style()
  2. Package Identity: PKG_NAMES for version detection
  3. Team Directives: TEAM_DIRECTIVES_DIRNAME
  4. Extension Namespaces: EXTENSION_NAMESPACES, EXTENSION_ALIAS_PATTERN_ENABLED

What Can Stay in init.py

The following tikalk-specific features are implemented directly in __init__.py because they require access to internal functions:

  1. install_bundled_extensions() - Installs bundled extensions during init
  2. install_bundled_presets() - Installs bundled presets during init
  3. get_preinstalled_extensions() - Returns list of pre-installed extensions
  4. sync_team_ai_directives() - Syncs team-ai-directives repository

These functions are called during init but don't conflict with upstream because they use conditional checks (e.g., if skip_bundled:).

Testing

Run tests to verify customization module works correctly:

python3 -c "
from src.specify_cli import ACCENT_COLOR, BANNER_COLORS, accent, accent_style
from src.specify_cli.extensions import EXTENSION_COMMAND_NAME_PATTERN

print('ACCENT_COLOR:', ACCENT_COLOR)
print('accent test:', accent('Test', bold=True))
print('Extension pattern:', EXTENSION_COMMAND_NAME_PATTERN.pattern)
"

Expected output:

ACCENT_COLOR: #f47721
accent test: [bold #f47721]Test[/]
Extension pattern: ^(?:speckit|adlc)\.([a-z0-9-]+)\.([a-z0-9-]+)$

Tag Management

IMPORTANT: The fork uses the tag pattern agentic-sdlc-v* to distinguish from upstream v* tags.

Why This Matters

Upstream uses tags like v0.5.0, v0.5.1, etc. These tags from origin trigger GitHub Actions workflows designed for upstream, which causes confusion and wasted CI runs.

Tag Naming Convention

  • Fork tags: agentic-sdlc-v0.3.11 (our releases)
  • Upstream tags: v0.5.0 (from upstream/main, don't push these to origin)

Before Pushing Tags

Always check before pushing to avoid triggering upstream workflows:

# Check what tags you're about to push
git push origin --dry-run --tags

# List only fork-pattern tags
git tag -l "agentic-sdlc-*"

Removing Stray Tags

If upstream tags get pushed to origin by mistake, remove them:

# Local cleanup
git tag -l "v*" | xargs -I {} git tag -d {}

# Remote cleanup (example for v0.5.0)
git push origin --delete v0.5.0

Or delete multiple at once:

git push origin --delete v0.0.1 v0.1.0 v0.2.0 v0.5.0

CI Troubleshooting

This section documents common CI failures and how to debug them.

Common Issues

1. Unresolved Merge Conflict Markers

Symptom: ruff fails with invalid-syntax: Expected ,, found <<

Example error:

invalid-syntax: Expected `,`, found `<<`
    --> src/specify_cli/__init__.py:1064:1
     |
1064 | <<<<<<< HEAD
     | ^^

Cause: Merge conflict not fully resolved - <<<<<<< HEAD markers remain in code

Fix:

# Find remaining conflict markers
grep -rn "<<<<<< HEAD" src/

# Fix in editor (remove the marker line)
# Verify fix
uvx ruff check src/

# Commit and push
git add -A
git commit -m "fix: remove unresolved merge conflict marker"
git push origin main

Prevention: Always run uvx ruff check src/ locally before pushing

2. Test Failures After Merge

Symptom: pytest fails after upstream merge

Fix:

# Run tests locally
uv run pytest

# If tests fail, investigate and fix
# Then commit and push

3. Python Version Compatibility

Symptom: Tests fail on specific Python versions in CI matrix

Fix: Ensure code is compatible with Python 3.11, 3.12, and 3.13

Debugging CI Failures

  1. Check the workflow run: Look at the GitHub Actions logs for the specific error
  2. Reproduce locally: Run the same commands locally:
    uvx ruff check src/
    uv run pytest
  3. Check for conflicts: grep -rn "<<<<<<" src/
  4. Review recent changes: git log --oneline -10

Lessons Learned from Upstream Merge

This section documents hard-won lessons from merging upstream changes.

Critical Rules

  1. NEVER use git checkout --theirs for pyproject.toml or init.py

    • Using --theirs on pyproject.toml discards the fork version entirely
    • The stash with fork changes gets lost
    • Always manually edit to preserve fork values after merge
  2. Use upstream as clean base, then ADD fork customizations

    • Merge upstream/main cleanly first
    • Then manually add fork-specific functions/values AFTER merge
    • This prevents accidental overwriting
  3. The import block is the SINGLE SOURCE OF TRUTH

    • All fork customizations must be in the try/except import block
    • Both the imports AND the fallback functions must be complete
    • Missing fallbacks = runtime errors when cli_customization.py is not available

pyproject.toml Wheel Paths

The fork uses specify_cli/core_pack/... paths (NOT root-level directories):

[tool.hatch.build.targets.wheel.force-include]
# Tikalk bundled extensions
"extensions/levelup" = "specify_cli/core_pack/extensions/levelup"
"extensions/evals" = "specify_cli/core_pack/extensions/evals"
"extensions/architect" = "specify_cli/core_pack/extensions/architect"
"extensions/quick" = "specify_cli/core_pack/extensions/quick"
"extensions/product" = "specify_cli/core_pack/extensions/product"
"extensions/tdd" = "specify_cli/core_pack/extensions/tdd"  # Don't forget!
# Tikalk bundled presets
"presets/agentic-sdlc" = "specify_cli/bundled_presets/agentic-sdlc"
# Core pack assets
"templates/agent-file-template.md" = "specify_cli/core_pack/templates/agent-file-template.md"
# ... etc

Common mistake: Using root-level paths like "extensions/levelup" = "extensions/levelup" will cause build errors because those paths don't exist inside the wheel.

init.py Required Customizations

After merging upstream, ensure these are present in init.py:

  1. Full import block (see above) - with ALL imports and fallbacks
  2. TEAM_DIRECTIVES_DIRNAME - for team-ai-directives feature
  3. _run_git_command() - helper for git operations
  4. sync_team_ai_directives() - clones/updates team repo
  5. compute_skill_output_name() - delegates to cli_customization
  6. TAGLINE - fork's tagline (different from upstream)

init.py Theming

Apply theming to all UI elements using accent_style() and accent():

  • show_banner(): Use BANNER_COLORS and accent_style() for tagline
  • StepTracker title: f"[{accent_style()}]{self.title}"
  • "Selected AI assistant:" and "Selected script type:": Use accent()
  • "Project ready.": Use accent(bold=True)
  • All Panel borders: Use border_style=accent_style()
  • Next Steps and Enhancement Commands panels

Tracker Steps

The init flow must include these steps in order:

for key, label in [
    ("chmod", "Ensure scripts executable"),
    ("constitution", "Constitution setup"),
    ("git", "Install git extension"),
    ("workflow", "Install bundled workflow"),
    ("team-directives", "Team AI Directives setup"),
    ("extensions", "Install bundled extensions"),
    ("presets", "Install bundled presets"),
]:
    tracker.add(key, label)

# Final MUST be added LAST
tracker.add("final", "Finalize")

Command Prefix

The fork uses /spec.* prefix instead of upstream's /speckit.*:

def _display_cmd(name: str) -> str:
    # ... agent-specific cases ...
    # Fork default: use "spec." prefix instead of "speckit."
    return f"/spec.{name}"

Backup Before Merge

Always create a backup branch BEFORE merging:

git branch backup-before-upstream-merge-$(date +%Y%m%d-%H%M%S)

This allows you to:

  • Compare against backup to see exactly what changed
  • Restore any accidentally lost customizations
  • Debug issues by comparing working vs broken state

Integration Test File Inventory Tests

The fork bundles extensions, presets, and skills that aren't present in upstream. This causes the test_complete_file_inventory_* tests to fail because they expect upstream's file count.

Solution: Skip these tests on the fork by checking PKG_NAMES:

def test_complete_file_inventory_sh(self, tmp_path):
    """Every file produced by specify init --integration <key> --script sh."""
    from specify_cli import PKG_NAMES
    if any("agentic-sdlc" in pkg for pkg in PKG_NAMES):
        import pytest
        pytest.skip("Fork has bundled extensions/presets with different file counts")
    # ... rest of test

This check exists in:

  • tests/integrations/test_integration_base_markdown.py
  • tests/integrations/test_integration_base_skills.py

The tests pass upstream (no skip) and skip on the fork (with skip), ensuring CI passes in both environments.