Skip to content

Commit 1875799

Browse files
committed
feat(sdd): add SDD infrastructure and runner as first managed component
Introduces Spec-Driven Development (SDD) enforcement infrastructure: - SDD manifest (.specify/sdd-manifest.yaml) declaring managed components and their paths, constitutions, specs, and enforcement mode - Runner constitution (.specify/constitutions/runner.md) with 6 principles extracted from PR #1091 patterns (version pinning, freshness automation, dependency procedures, layer discipline, schema sync, bridge modularity) - Runner spec (.specify/specs/runner.md) documenting component boundary, current state, maintenance workflows, and change protocol - SDD preflight CI job (.github/workflows/sdd-preflight.yml) that checks PRs for managed-path modifications and warns or blocks accordingly The runner is the first component onboarded to SDD in warn mode. Additional components can be added to the manifest to gradually migrate the codebase. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 8ac0f85 commit 1875799

4 files changed

Lines changed: 403 additions & 0 deletions

File tree

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
name: SDD Preflight
2+
3+
on:
4+
pull_request:
5+
types: [opened, synchronize, reopened, labeled, unlabeled]
6+
7+
permissions:
8+
pull-requests: write
9+
contents: read
10+
11+
jobs:
12+
check-managed-paths:
13+
name: SDD boundary check
14+
runs-on: ubuntu-latest
15+
timeout-minutes: 2
16+
# Skip entirely if PR has sdd-exempt label
17+
if: ${{ !contains(github.event.pull_request.labels.*.name, 'sdd-exempt') }}
18+
19+
steps:
20+
- name: Checkout repository
21+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
22+
23+
- name: Check SDD boundaries
24+
id: check
25+
env:
26+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
27+
PR_NUMBER: ${{ github.event.pull_request.number }}
28+
run: |
29+
set -euo pipefail
30+
31+
MANIFEST=".specify/sdd-manifest.yaml"
32+
if [ ! -f "$MANIFEST" ]; then
33+
echo "No SDD manifest found, skipping"
34+
echo "violation=false" >> "$GITHUB_OUTPUT"
35+
echo "has_findings=false" >> "$GITHUB_OUTPUT"
36+
exit 0
37+
fi
38+
39+
# Get changed files in this PR
40+
CHANGED_FILES=$(gh pr diff "$PR_NUMBER" --name-only)
41+
if [ -z "$CHANGED_FILES" ]; then
42+
echo "No changed files, skipping"
43+
echo "violation=false" >> "$GITHUB_OUTPUT"
44+
echo "has_findings=false" >> "$GITHUB_OUTPUT"
45+
exit 0
46+
fi
47+
48+
# Parse all managed components in a single yq call:
49+
# Output format: component<TAB>mode<TAB>path (one line per path)
50+
DEFAULT_MODE=$(yq '.default-mode // "warn"' "$MANIFEST")
51+
COMPONENT_PATHS=$(yq -r '
52+
.managed-components | to_entries[] |
53+
.key as $comp |
54+
(.value.mode // "'"$DEFAULT_MODE"'") as $mode |
55+
.value.paths[] |
56+
$comp + "\t" + $mode + "\t" + .
57+
' "$MANIFEST")
58+
59+
if [ -z "$COMPONENT_PATHS" ]; then
60+
echo "No managed paths defined, skipping"
61+
echo "violation=false" >> "$GITHUB_OUTPUT"
62+
echo "has_findings=false" >> "$GITHUB_OUTPUT"
63+
exit 0
64+
fi
65+
66+
# Convert glob patterns to grep regexes and build a lookup file
67+
# Format: regex<TAB>component<TAB>mode
68+
PATTERN_FILE=$(mktemp)
69+
while IFS=$'\t' read -r comp mode pattern; do
70+
# Escape regex special chars in the pattern, then convert globs
71+
regex=$(printf '%s' "$pattern" \
72+
| sed 's/[.+^${}()|[\]]/\\&/g' \
73+
| sed 's/\*\*/.*/g' \
74+
| sed 's/\*/[^\/]*/g')
75+
printf '%s\t%s\t%s\n' "$regex" "$comp" "$mode" >> "$PATTERN_FILE"
76+
done <<< "$COMPONENT_PATHS"
77+
78+
# Match changed files against patterns
79+
VIOLATIONS=""
80+
WARNINGS=""
81+
82+
while IFS= read -r changed_file; do
83+
[ -z "$changed_file" ] && continue
84+
while IFS=$'\t' read -r regex comp mode; do
85+
if printf '%s' "$changed_file" | grep -qE "^${regex}$"; then
86+
row="| \`${changed_file}\` | **${comp}** | ${mode} |"
87+
if [ "$mode" = "enforce" ]; then
88+
VIOLATIONS="${VIOLATIONS}${row}"$'\n'
89+
else
90+
WARNINGS="${WARNINGS}${row}"$'\n'
91+
fi
92+
break
93+
fi
94+
done < "$PATTERN_FILE"
95+
done <<< "$CHANGED_FILES"
96+
97+
rm -f "$PATTERN_FILE"
98+
99+
# Determine result
100+
if [ -n "$VIOLATIONS" ]; then
101+
echo "violation=true" >> "$GITHUB_OUTPUT"
102+
else
103+
echo "violation=false" >> "$GITHUB_OUTPUT"
104+
fi
105+
106+
if [ -n "$WARNINGS" ] || [ -n "$VIOLATIONS" ]; then
107+
echo "has_findings=true" >> "$GITHUB_OUTPUT"
108+
else
109+
echo "has_findings=false" >> "$GITHUB_OUTPUT"
110+
fi
111+
112+
# Build comment body and write to a file (avoids shell injection)
113+
BODY_FILE=$(mktemp)
114+
if [ -n "$VIOLATIONS" ]; then
115+
cat > "$BODY_FILE" <<COMMENTEOF
116+
<!-- sdd-preflight -->
117+
## ⛔ SDD Preflight — Boundary Violation
118+
119+
This PR modifies files in SDD-managed component(s) that require changes to go through the designated agent workflow.
120+
121+
| File | Component | Mode |
122+
|------|-----------|------|
123+
${VIOLATIONS}
124+
**Action required**: These components are in \`enforce\` mode. Please use the component's agent workflow to make these changes, or request an exemption by adding the \`sdd-exempt\` label.
125+
126+
📖 See [SDD Manifest](.specify/sdd-manifest.yaml) for details.
127+
COMMENTEOF
128+
elif [ -n "$WARNINGS" ]; then
129+
cat > "$BODY_FILE" <<COMMENTEOF
130+
<!-- sdd-preflight -->
131+
## ⚠️ SDD Preflight — Managed Paths Modified
132+
133+
This PR modifies files in SDD-managed component(s). These components are migrating to Spec-Driven Development.
134+
135+
| File | Component | Mode |
136+
|------|-----------|------|
137+
${WARNINGS}
138+
**No action required** — these components are in \`warn\` mode. Consider using the component's agent workflow for future changes.
139+
140+
📖 Specs: [Runner Spec](.specify/specs/runner.md) · [Runner Constitution](.specify/constitutions/runner.md)
141+
COMMENTEOF
142+
fi
143+
144+
echo "body_file=$BODY_FILE" >> "$GITHUB_OUTPUT"
145+
146+
- name: Comment on PR
147+
if: steps.check.outputs.has_findings == 'true'
148+
env:
149+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
150+
PR_NUMBER: ${{ github.event.pull_request.number }}
151+
run: |
152+
# Delete previous SDD preflight comments (identified by HTML marker)
153+
gh api --paginate "repos/${{ github.repository }}/issues/${PR_NUMBER}/comments" \
154+
--jq '.[] | select(.body | contains("<!-- sdd-preflight -->")) | .id' \
155+
| while read -r comment_id; do
156+
gh api -X DELETE "repos/${{ github.repository }}/issues/comments/${comment_id}" 2>/dev/null || true
157+
done
158+
159+
gh pr comment "$PR_NUMBER" --body-file "${{ steps.check.outputs.body_file }}"
160+
161+
- name: Enforce SDD boundaries
162+
if: steps.check.outputs.violation == 'true'
163+
run: |
164+
echo "::error::SDD boundary violation detected. See PR comment for details."
165+
echo "::error::Add the 'sdd-exempt' label to bypass this check."
166+
exit 1

.specify/constitutions/runner.md

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# Runner Constitution
2+
3+
**Version**: 1.0.0
4+
**Ratified**: 2026-03-28
5+
**Parent**: [ACP Platform Constitution](../memory/constitution.md)
6+
7+
This constitution governs the `components/runners/ambient-runner/` component and its supporting CI workflows. It inherits all principles from the platform constitution and adds runner-specific constraints.
8+
9+
---
10+
11+
## Principle R-I: Version Pinning
12+
13+
All external tools installed in the runner image MUST be version-pinned.
14+
15+
- CLI tools (gh, glab) MUST use `ARG <TOOL>_VERSION=X.Y.Z` in the Dockerfile and be installed via pinned binary downloads — never from unpinned package repos.
16+
- Python packages (uv, pre-commit) MUST use `==X.Y.Z` pins at install time.
17+
- npm packages (gemini-cli) MUST use `@X.Y.Z` pins.
18+
- The base image MUST be pinned by SHA digest.
19+
- Versions MUST be declared as Dockerfile `ARG`s at the top of the file for automated bumping.
20+
21+
**Rationale**: Unpinned installs cause non-reproducible builds and silent regressions. Pinning enables automated freshness tracking and controlled upgrades.
22+
23+
## Principle R-II: Automated Freshness
24+
25+
Runner tool versions MUST be checked for staleness automatically.
26+
27+
- The `runner-tool-versions.yml` workflow runs weekly and on manual dispatch.
28+
- It checks all pinned components against upstream registries.
29+
- When updates are available, it opens a single PR with a version table.
30+
- The workflow MUST NOT auto-merge; a human or authorized agent reviews.
31+
32+
**Rationale**: Pinned versions go stale. Automated freshness checks balance reproducibility with security and feature currency.
33+
34+
## Principle R-III: Dependency Update Procedure
35+
36+
Dependency updates MUST follow the documented procedure in `docs/UPDATE_PROCEDURE.md`.
37+
38+
- Python dependencies use `>=X.Y.Z` floor pins in pyproject.toml, resolved by `uv lock`.
39+
- SDK bumps (claude-agent-sdk) MUST trigger a review of the frontend Agent Options schema for drift.
40+
- Base image major version upgrades (e.g., UBI 9 → 10) require manual testing.
41+
- Lock files MUST be regenerated after any pyproject.toml change.
42+
43+
**Rationale**: A structured procedure prevents partial updates, version conflicts, and schema drift between backend SDK types and frontend forms.
44+
45+
## Principle R-IV: Image Layer Discipline
46+
47+
Dockerfile layers MUST be optimized for size and cacheability.
48+
49+
- System packages (`dnf install`) SHOULD be consolidated into a single `RUN` layer.
50+
- Build-only dependencies (e.g., `python3-devel`) MUST be removed in the same layer where they are last used, not in a separate layer.
51+
- Binary CLI downloads (gh, glab) SHOULD share a single `RUN` layer to avoid redundant arch detection.
52+
- `dnf clean all` and cache removal MUST happen in the same `RUN` as the install.
53+
54+
**Rationale**: Docker layers are additive. Removing packages in a later layer doesn't reclaim space — it only adds whiteout entries.
55+
56+
## Principle R-V: Agent Options Schema Sync
57+
58+
The frontend Agent Options form MUST stay in sync with the claude-agent-sdk types.
59+
60+
- `schema.ts` defines the Zod schema matching `ClaudeAgentOptions` from the SDK.
61+
- `options-form.tsx` renders the form from the schema.
62+
- Editor components in `_components/` MUST use stable React keys (ref-based IDs) for record/map editors to prevent focus loss on rename.
63+
- Record editors MUST prevent key collisions on add operations.
64+
- The form is gated behind the `advanced-agent-options` Unleash flag.
65+
66+
**Rationale**: Schema drift between SDK and frontend creates silent data loss or validation errors. Stable keys prevent UX bugs in dynamic form editors.
67+
68+
## Principle R-VI: Bridge Modularity
69+
70+
Agent bridges (Claude, Gemini, LangGraph) MUST be isolated modules.
71+
72+
- Each bridge lives in `ambient_runner/bridges/<name>/`.
73+
- Bridges MUST NOT import from each other.
74+
- Shared logic lives in `ambient_runner/` (bridge.py, platform/).
75+
- New bridges follow the same directory structure and registration pattern.
76+
77+
**Rationale**: Bridge isolation enables independent testing, deployment, and addition of new AI providers without cross-contamination.
78+
79+
---
80+
81+
## Governance
82+
83+
- This constitution is versioned using semver.
84+
- Amendments require a PR that updates this file and passes the SDD preflight check.
85+
- The platform constitution takes precedence on any conflict.
86+
- Compliance is reviewed as part of runner-related PR reviews.

.specify/sdd-manifest.yaml

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# SDD Manifest — Spec-Driven Development Enforcement
2+
#
3+
# Components listed here are governed by their spec-kit constitution and spec.
4+
# Changes to managed paths MUST go through the designated agent workflow.
5+
# The sdd-preflight CI job enforces this boundary.
6+
#
7+
# To add a new component:
8+
# 1. Create its constitution in .specify/constitutions/<component>.md
9+
# 2. Create its spec in .specify/specs/<component>.md
10+
# 3. Add an entry below with paths, spec, constitution, and agent
11+
# 4. The preflight job will begin enforcing on the next PR
12+
13+
version: 1
14+
15+
# Platform-wide constitution (all components inherit from this)
16+
platform-constitution: .specify/memory/constitution.md
17+
18+
# Enforcement mode for new components during migration
19+
# "warn" = comment on PR but don't block; "enforce" = required check
20+
default-mode: warn
21+
22+
managed-components:
23+
runner:
24+
description: >
25+
Python runner executing Claude Code CLI in Job pods.
26+
Manages AG-UI adapter, MCP integrations, and agent bridges.
27+
paths:
28+
- components/runners/ambient-runner/**
29+
- components/frontend/src/components/claude-agent-options/**
30+
- .github/workflows/runner-tool-versions.yml
31+
constitution: .specify/constitutions/runner.md
32+
spec: .specify/specs/runner.md
33+
mode: warn
34+
added-in-pr: 1091
35+
# Future: when a GitHub App or bot account is set up for the agent,
36+
# set agent-login to its GitHub username for authorship checks.
37+
# agent-login: ambient-runner-agent[bot]
38+
39+
# Uncomment to onboard the next component:
40+
# backend:
41+
# description: Go REST API (Gin), manages K8s Custom Resources
42+
# paths:
43+
# - components/backend/**
44+
# constitution: .specify/constitutions/backend.md
45+
# spec: .specify/specs/backend.md
46+
# mode: warn
47+
# added-in-pr: TBD

0 commit comments

Comments
 (0)