Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 59 additions & 1 deletion .github/workflows/pr-orchestrator.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ jobs:
- uses: actions/checkout@v4
with:
fetch-depth: 0
with:
fetch-depth: 0
- uses: dorny/paths-filter@v3
id: filter
with:
Expand All @@ -64,9 +66,42 @@ jobs:
echo "skip_tests_dev_to_main=false" >> "$GITHUB_OUTPUT"
fi

verify-module-signatures:
name: Verify Module Signatures
needs: [changes]
if: needs.changes.outputs.code_changed == 'true'
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v4

- name: Set up Python 3.12
uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: "pip"

- name: Install verifier dependencies
run: |
python -m pip install --upgrade pip
python -m pip install pyyaml cryptography cffi

- name: Verify bundled module checksums and signatures
run: |
BASE_REF=""
if [ "${{ github.event_name }}" = "pull_request" ]; then
BASE_REF="origin/${{ github.event.pull_request.base.ref }}"
fi
if [ -n "$BASE_REF" ]; then
python scripts/verify-modules-signature.py --require-signature --enforce-version-bump --version-check-base "$BASE_REF"
else
python scripts/verify-modules-signature.py --require-signature --enforce-version-bump
fi

tests:
name: Tests (Python 3.12)
needs: [changes]
needs: [changes, verify-module-signatures]
if: needs.changes.outputs.code_changed == 'true'
outputs:
run_unit_coverage: ${{ steps.detect-unit.outputs.run_unit_coverage }}
Expand Down Expand Up @@ -583,6 +618,29 @@ jobs:
run: |
chmod +x .github/workflows/scripts/generate-release-notes.sh
chmod +x .github/workflows/scripts/create-github-release.sh
chmod +x scripts/sign-module.sh

- name: Sign bundled module manifests (release hardening)
env:
SPECFACT_MODULE_PRIVATE_SIGN_KEY: ${{ secrets.SPECFACT_MODULE_PRIVATE_SIGN_KEY }}
SPECFACT_MODULE_PRIVATE_SIGN_KEY_PASSPHRASE: ${{ secrets.SPECFACT_MODULE_PRIVATE_SIGN_KEY_PASSPHRASE }}
run: |
if [ -z "${SPECFACT_MODULE_PRIVATE_SIGN_KEY}" ]; then
echo "❌ Missing required secret: SPECFACT_MODULE_PRIVATE_SIGN_KEY"
exit 1
fi
if [ -z "${SPECFACT_MODULE_PRIVATE_SIGN_KEY_PASSPHRASE}" ]; then
echo "❌ Missing required secret: SPECFACT_MODULE_PRIVATE_SIGN_KEY_PASSPHRASE"
exit 1
fi
python -m pip install --upgrade pip
python -m pip install pyyaml cryptography cffi
mapfile -t MANIFESTS < <(find src/specfact_cli/modules -name 'module-package.yaml' -type f)
if [ "${#MANIFESTS[@]}" -eq 0 ]; then
echo "No bundled module manifests found to sign."
exit 0
fi
python scripts/sign-modules.py "${MANIFESTS[@]}"

- name: Get version from PyPI publish step
id: get_version
Expand Down
99 changes: 86 additions & 13 deletions .github/workflows/sign-modules.yml
Original file line number Diff line number Diff line change
@@ -1,29 +1,102 @@
# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
# Sign module manifests for integrity (arch-06). Outputs checksums for manifest integrity fields.
name: Sign Modules
# Harden module signing by enforcing strict verification and deterministic signing output checks.
name: Module Signature Hardening

on:
workflow_dispatch: {}
push:
branches: [main]
branches: [dev, main]
paths:
- "src/specfact_cli/modules/**/module-package.yaml"
- "modules/**/module-package.yaml"
- "src/specfact_cli/modules/**"
- "modules/**"
- "resources/keys/**"
- "scripts/sign-modules.py"
- "scripts/verify-modules-signature.py"
- ".github/workflows/sign-modules.yml"
pull_request:
branches: [dev, main]
paths:
- "src/specfact_cli/modules/**"
- "modules/**"
- "resources/keys/**"
- "scripts/sign-modules.py"
- "scripts/verify-modules-signature.py"
- ".github/workflows/sign-modules.yml"

jobs:
sign:
name: Sign module manifests
verify:
name: Verify module signatures
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Sign module manifests
- name: Install signer dependencies
run: |
for f in $(find . -name 'module-package.yaml' -not -path './.git/*' 2>/dev/null | head -20); do
if [ -f "scripts/sign-module.sh" ]; then
bash scripts/sign-module.sh "$f" || true
fi
done
python -m pip install --upgrade pip
python -m pip install pyyaml cryptography cffi

- name: Verify bundled module signatures
run: |
BASE_REF=""
if [ "${{ github.event_name }}" = "pull_request" ]; then
BASE_REF="origin/${{ github.event.pull_request.base.ref }}"
fi
if [ -n "$BASE_REF" ]; then
python scripts/verify-modules-signature.py --require-signature --enforce-version-bump --version-check-base "$BASE_REF"
else
python scripts/verify-modules-signature.py --require-signature --enforce-version-bump
fi

reproducibility:
name: Assert signing reproducibility
runs-on: ubuntu-latest
needs: [verify]
permissions:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Install signer dependencies
run: |
python -m pip install --upgrade pip
python -m pip install pyyaml cryptography cffi

- name: Re-sign manifests and assert no diff
env:
SPECFACT_MODULE_PRIVATE_SIGN_KEY: ${{ secrets.SPECFACT_MODULE_PRIVATE_SIGN_KEY }}
SPECFACT_MODULE_PRIVATE_SIGN_KEY_PASSPHRASE: ${{ secrets.SPECFACT_MODULE_PRIVATE_SIGN_KEY_PASSPHRASE }}
run: |
if [ -z "${SPECFACT_MODULE_PRIVATE_SIGN_KEY}" ]; then
echo "::notice::Skipping reproducibility check because SPECFACT_MODULE_PRIVATE_SIGN_KEY is not configured."
exit 0
fi

mapfile -t MANIFESTS < <(find src/specfact_cli/modules modules -name 'module-package.yaml' -type f 2>/dev/null | sort)
if [ "${#MANIFESTS[@]}" -eq 0 ]; then
echo "No module manifests found"
exit 0
fi

python scripts/sign-modules.py "${MANIFESTS[@]}"

if ! git diff --exit-code -- src/specfact_cli/modules modules; then
echo "::error::Module signatures are stale for the configured signing key. Re-sign and commit manifest updates."
git --no-pager diff --name-only -- src/specfact_cli/modules modules
exit 1
fi
19 changes: 19 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,25 @@ Run all steps in order before committing. Every step must pass with no errors.
5. `hatch run contract-test` # contract-first validation
6. `hatch run smart-test` # targeted test run (use `smart-test-full` for larger modifications)

### Module Signature Gate (Required for Change Finalization)

Before PR creation, every change MUST pass bundled module signature verification:

1. Run `hatch run ./scripts/verify-modules-signature.py --require-signature`.
2. If verification fails because module contents changed, re-sign affected manifests:
- `hatch run python scripts/sign-modules.py --key-file <private-key.pem> <module-package.yaml ...>`
3. Re-run verification until green.

Rules:

- Do not merge/PR with stale or missing integrity metadata for bundled modules.
- Treat signature verification as a quality gate equal to lint/type-check/tests.
- Module version bump is mandatory before signing changed module contents. Do not keep the same module version when module files or signatures change.
- For any module re-sign/sign operation, increment module version using semver (major/minor/patch) so published/registered versions are immutable.
- Use signer/verifier enforcement paths:
- signer rejects changed modules with unchanged version by default;
- verifier/CI enforces version-bump checks for changed manifests.

### OpenSpec Workflow

Before modifying application code, **always** verify that an active OpenSpec change in `openspec/changes/` **explicitly covers the requested modification**. This is the spec-driven workflow defined in `openspec/config.yaml`. Skip only when the user explicitly says `"skip openspec"` or `"implement without openspec change"`.
Expand Down
34 changes: 34 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,38 @@ All notable changes to this project will be documented in this file.
**Important:** Changes need to be documented below this block as this is the header section. Each section should be separated by a horizontal rule. Newer changelog entries need to be added on top of prior ones to keep the history chronological with most recent changes first.


---

## [0.37.0] - 2026-02-23

### Added

- Bundled module signing/verification now covers full module payload contents (all files in module directory), not only manifest fields.
- `scripts/sign-module.sh` / `scripts/sign-modules.py` now support encrypted private keys with passphrase input via `--passphrase`, `--passphrase-stdin`, or `SPECFACT_MODULE_PRIVATE_SIGN_KEY_PASSPHRASE`.
- CI signing/verification workflow wiring now uses dedicated secrets `SPECFACT_MODULE_PRIVATE_SIGN_KEY` and `SPECFACT_MODULE_PRIVATE_SIGN_KEY_PASSPHRASE`.
- Signature verification tooling now supports module-version policy checks (`--enforce-version-bump`, `--version-check-base`) to prevent re-signing changed contents under unchanged versions.

### Changed

- `specfact init` output now explicitly points users to `specfact module` for module lifecycle commands.
- `specfact module install` / `uninstall` now support explicit scope targeting (`user` or `project`) with `--repo` for project scope.
- `specfact module install` command/help now documents and supports bundled-source resolution controls so users can install shipped modules selectively through the same lifecycle flow as marketplace installs.

### Fixed

- `specfact init` now seeds shipped module artifacts into `~/.specfact/modules`, so commands contributed by shipped modules (for example `specfact backlog add`) no longer depend on repository-local `modules/` folders.
- Module installer/discovery now recognizes `~/.specfact/modules` as a canonical per-user root while remaining backward-compatible with legacy module roots.
- Workspace-local module discovery is now restricted to `<repo>/.specfact/modules` (not `<repo>/modules`), preventing accidental ownership of arbitrary repository folders.
- In repository context, project modules from `<repo>/.specfact/modules` now take precedence over user modules from `~/.specfact/modules`.
- Added `specfact module init --scope project [--repo PATH]` so bundled modules can be seeded per-project, while default `specfact module init` continues to seed user scope.
- Startup checks now include bundled-module freshness guidance on CLI version change and at most once per 24 hours, with actionable commands for project and user scopes.
- Removed deprecated `specfact init` lifecycle flags (`--list-modules`, `--enable-module`, `--disable-module`) so module lifecycle management lives only under `specfact module`.
- Added `specfact module list --show-bundled-available` to display bundled modules that are available locally but not yet installed, with user/project scope install hints.
- `specfact module install` now resolves bundled modules before marketplace fallback, enabling subset install of shipped bundles.
- `specfact module uninstall` now blocks ambiguous removals when module IDs exist in both user and project roots unless `--scope` is explicitly selected.
- Module integrity runtime checks now avoid transient runtime artifacts (for example Python cache files) so installed modules do not fail trust checks due to local generated files.
- Uninstall now correctly resolves legacy marketplace install roots when applicable, preventing false-success uninstall outcomes during upgrades.

---

## [0.36.1] - 2026-02-23
Expand Down Expand Up @@ -111,6 +143,8 @@ All notable changes to this project will be documented in this file.
- `docs/index.md`
- Simplified top-level `README.md` by removing deep architecture implementation details and linking technical readers to architecture docs.

### Fixed

---

## [0.33.0] - 2026-02-17
Expand Down
2 changes: 1 addition & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,6 @@ SpecFact CLI uses a lifecycle-managed module system:
- `specfact init` bootstraps local state.
- `specfact init ide` handles IDE prompt/template installation and updates.
- `specfact module` is the canonical lifecycle surface for install/list/show/search/enable/disable/uninstall/upgrade.
- `specfact init --list-modules`, `--enable-module`, and `--disable-module` remain compatibility aliases.
- Dependency and compatibility guards prevent invalid module states; `--force` enables dependency-aware cascades.

This is the baseline for marketplace-driven module lifecycle and future community module distribution.
Expand All @@ -106,6 +105,7 @@ For implementation details, see:
- [Module Contracts](reference/module-contracts.md)
- [Installing Modules](guides/installing-modules.md)
- [Module Marketplace](guides/module-marketplace.md)
- [Module Signing and Key Rotation](guides/module-signing-and-key-rotation.md)

---

Expand Down
2 changes: 2 additions & 0 deletions docs/_layouts/default.html
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ <h2 class="docs-sidebar-title">
<ul>
<li><a href="{{ '/getting-started/installation/' | relative_url }}">Installation</a></li>
<li><a href="{{ '/getting-started/first-steps/' | relative_url }}">First Steps</a></li>
<li><a href="{{ '/getting-started/module-bootstrap-checklist/' | relative_url }}">Module Bootstrap Checklist</a></li>
<li><a href="{{ '/getting-started/tutorial-backlog-refine-ai-ide/' | relative_url }}">Tutorial: Backlog Refine with AI IDE</a></li>
<li><a href="{{ '/getting-started/tutorial-daily-standup-sprint-review/' | relative_url }}">Tutorial: Daily Standup and Sprint Review</a></li>
</ul>
Expand All @@ -148,6 +149,7 @@ <h2 class="docs-sidebar-title">
<li><a href="{{ '/guides/extending-projectbundle/' | relative_url }}">Extending ProjectBundle</a></li>
<li><a href="{{ '/guides/installing-modules/' | relative_url }}">Installing Modules</a></li>
<li><a href="{{ '/guides/module-marketplace/' | relative_url }}">Module Marketplace</a></li>
<li><a href="{{ '/guides/module-signing-and-key-rotation/' | relative_url }}">Module Signing and Key Rotation</a></li>
<li><a href="{{ '/guides/using-module-security-and-extensions/' | relative_url }}">Using Module Security and Extensions</a></li>
<li><a href="{{ '/brownfield-engineer/' | relative_url }}">Working With Existing Code</a></li>
<li><a href="{{ '/brownfield-journey/' | relative_url }}">Existing Code Journey</a></li>
Expand Down
1 change: 1 addition & 0 deletions docs/getting-started/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ uvx specfact-cli@latest plan init my-project --interactive

- πŸ“– **[Installation Guide](installation.md)** - Install SpecFact CLI
- πŸ“– **[First Steps](first-steps.md)** - Step-by-step first commands
- πŸ“– **[Module Bootstrap Checklist](module-bootstrap-checklist.md)** - Verify bundled modules are installed in user/project scope
- πŸ“– **[Tutorial: Using SpecFact with OpenSpec or Spec-Kit](tutorial-openspec-speckit.md)** ⭐ **NEW** - Complete beginner-friendly tutorial
- πŸ“– **[DevOps Backlog Integration](../guides/devops-adapter-integration.md)** πŸ†• **NEW FEATURE** - Integrate SpecFact into agile DevOps workflows
- πŸ“– **[Backlog Refinement](../guides/backlog-refinement.md)** πŸ†• **NEW FEATURE** - AI-assisted template-driven refinement for standardizing work items
Expand Down
Loading