Skip to content

Commit c36a840

Browse files
committed
chore: initial spec-forge project import
- Import the spec-forge skill family and staged workflow assets. - Add the Rust spec-forge-cli implementation, embedded templates, and validation tests. - Include package documentation, npm release automation, and GitHub Actions workflows.
0 parents  commit c36a840

96 files changed

Lines changed: 22440 additions & 0 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.cli-forge/cli-plan.yml

Lines changed: 498 additions & 0 deletions
Large diffs are not rendered by default.

.cli-forge/design-contract.yml

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
skill_name: "spec-forge-cli"
2+
purpose: "Dedicated Rust CLI package for operating the spec-forge YAML workflow through one stable command surface instead of direct script entrypoints."
3+
positioning: "spec-forge-cli is the unified CLI front door for the existing spec-forge workflow family and workflow engine. It replaces script-by-script operation with a single executable that preserves the current YAML-first, stage-gated, broad-to-fine refinement model while making the system easier to discover, run, validate, package, and document. Relative to the current script collection, the CLI is positioned as a productized operator and agent interface, not a redesign of the underlying workflow semantics or artifact model."
4+
5+
sync_surfaces:
6+
- "spec-forge-cli/Cargo.toml package.description"
7+
- "spec-forge-cli/SKILL.md purpose section"
8+
- "spec-forge-cli/README.md header and description"
9+
- "help text summary line across non-leaf auto-help, `--help`, and structured `help`"
10+
- "release notes summary (when publish is in scope)"
11+
- "npm package description (when publish is in scope)"
12+
13+
help_contract:
14+
leaf_missing_required_input: "structured_error"
15+
non_leaf_default: "human_readable_help"
16+
help_flag: "human_readable_help"
17+
help_subcommand: "structured_help"
18+
human_readable_help:
19+
style: "man_like"
20+
section_order:
21+
- "NAME"
22+
- "SYNOPSIS"
23+
- "DESCRIPTION"
24+
- "OPTIONS"
25+
- "FORMATS"
26+
- "EXAMPLES"
27+
- "EXIT CODES"
28+
29+
publish_contract:
30+
stage: "publish"
31+
channels:
32+
- "repo_native"
33+
- "npm"
34+
35+
approved_by: "user"
36+
approved_at: "2026-04-21T06:17:46Z"

.cli-forge/handoff.yml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
stage_from: "router"
2+
stage_to: "design"
3+
timestamp: "2026-04-21T06:01:13Z"
4+
classification: "design"
5+
6+
inputs:
7+
skill_name: "spec-forge"
8+
project_path: "/Volumes/Documents/spec-forge"
9+
feature: ""
10+
publish_mode: ""
11+
post_adoption_objective: ""
12+
takeover_mode: ""
13+
14+
validation_status: "missing"
15+
notes: "Route to Design. The target repository already contains the spec-forge skill family and Python automation scripts, but it does not yet have a .cli-forge contract baseline or an existing Rust CLI project (no Cargo.toml or src/main.rs observed). The requested outcome is a dedicated CLI that replaces the current scripts, so the earliest valid stage is Design to define the new CLI skill identity and contract before planning."

.github/workflows/release.yml

Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
name: Release
2+
3+
on:
4+
push:
5+
branches: [main]
6+
workflow_dispatch:
7+
inputs:
8+
recover-version:
9+
description: "Recover a wedged release: set to the version string (e.g. 1.2.3) to republish npm packages for an existing tag without running semantic-release."
10+
required: false
11+
default: ""
12+
recover-run-id:
13+
description: "Optional original release run ID. When set, downloads the exact build artifacts from that run instead of rebuilding."
14+
required: false
15+
default: ""
16+
17+
permissions:
18+
actions: read
19+
contents: write
20+
id-token: write
21+
issues: write
22+
pull-requests: write
23+
24+
concurrency:
25+
group: release-${{ github.ref }}
26+
cancel-in-progress: false
27+
28+
env:
29+
CARGO_TERM_COLOR: always
30+
RUST_BACKTRACE: 1
31+
32+
defaults:
33+
run:
34+
shell: bash
35+
working-directory: spec-forge-cli
36+
37+
jobs:
38+
release:
39+
runs-on: macos-14
40+
env:
41+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
42+
steps:
43+
- name: Checkout
44+
uses: actions/checkout@v6
45+
with:
46+
fetch-depth: 0
47+
ref: ${{ inputs['recover-version'] != null && inputs['recover-version'] != '' && format('refs/tags/v{0}', inputs['recover-version']) || '' }}
48+
49+
- name: Setup Node.js
50+
uses: actions/setup-node@v6
51+
with:
52+
node-version: 24
53+
registry-url: https://registry.npmjs.org
54+
oidc-match: ${{ vars.NPM_PUBLISHER_PATTERN || '*' }}
55+
56+
- name: Read release config
57+
id: cfg
58+
run: |
59+
set -euo pipefail
60+
targets=$(node -e "const c=require('./release/config.json');console.log(c.targets.map(t=>t.rustTarget).join(','))")
61+
cli=$(node -e "const c=require('./release/config.json');console.log(c.cliName)")
62+
echo "targets=$targets" >> "$GITHUB_OUTPUT"
63+
echo "cli=$cli" >> "$GITHUB_OUTPUT"
64+
65+
- name: Setup Rust
66+
uses: dtolnay/rust-toolchain@stable
67+
with:
68+
components: rustfmt, clippy
69+
targets: ${{ steps.cfg.outputs.targets }}
70+
71+
- name: Restore Rust cache
72+
uses: Swatinem/rust-cache@v2
73+
with:
74+
workspaces: "spec-forge-cli -> target"
75+
76+
- name: Setup cross-build environment
77+
uses: ./spec-forge-cli/.github/actions/setup-build-env
78+
79+
- name: Install release harness dependencies
80+
run: npm ci
81+
82+
- name: Verify release config
83+
run: node scripts/release/validate-config.mjs
84+
85+
- name: Rust quality gates
86+
if: ${{ inputs['recover-version'] == null || inputs['recover-version'] == '' }}
87+
run: |
88+
set -euo pipefail
89+
cargo fmt --all -- --check
90+
cargo clippy --all-targets --all-features -- -D warnings
91+
cargo test
92+
93+
- name: Download original build artifacts
94+
if: ${{ inputs['recover-version'] != null && inputs['recover-version'] != '' && inputs['recover-run-id'] != null && inputs['recover-run-id'] != '' }}
95+
uses: actions/download-artifact@v4
96+
with:
97+
name: release-dist
98+
path: spec-forge-cli/dist/
99+
run-id: ${{ inputs['recover-run-id'] }}
100+
101+
- name: Resolve checked-out commit SHA
102+
if: ${{ inputs['recover-version'] != null && inputs['recover-version'] != '' && inputs['recover-run-id'] != null && inputs['recover-run-id'] != '' }}
103+
id: checkout-sha
104+
run: echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
105+
106+
- name: Verify artifact provenance
107+
if: ${{ inputs['recover-version'] != null && inputs['recover-version'] != '' && inputs['recover-run-id'] != null && inputs['recover-run-id'] != '' }}
108+
run: |
109+
node -e "
110+
const fs = require('fs');
111+
const provPath = './dist/provenance.json';
112+
if (!fs.existsSync(provPath)) {
113+
console.log('No provenance.json found, skipping verification.');
114+
process.exit(0);
115+
}
116+
const p = JSON.parse(fs.readFileSync(provPath, 'utf8'));
117+
const expectedVersion = process.env.EXPECTED_VERSION;
118+
const expectedSha = process.env.EXPECTED_SHA;
119+
const expectedRepo = process.env.EXPECTED_REPO;
120+
if (p.version !== expectedVersion) {
121+
console.error('Artifact version ' + p.version + ' does not match requested ' + expectedVersion + '.');
122+
process.exit(1);
123+
}
124+
if (p.commitSha && p.commitSha !== expectedSha) {
125+
console.error('Artifact commit ' + p.commitSha + ' does not match checkout ' + expectedSha + '.');
126+
process.exit(1);
127+
}
128+
if (p.repository && p.repository !== expectedRepo) {
129+
console.error('Artifact repository ' + p.repository + ' does not match ' + expectedRepo + '.');
130+
process.exit(1);
131+
}
132+
console.log('Provenance OK: v' + p.version + ' @ ' + (p.commitSha || 'unknown'));
133+
"
134+
env:
135+
EXPECTED_VERSION: ${{ inputs['recover-version'] }}
136+
EXPECTED_SHA: ${{ steps.checkout-sha.outputs.sha }}
137+
EXPECTED_REPO: ${{ github.repository }}
138+
139+
- name: Verify already-complete recovery
140+
if: ${{ inputs['recover-version'] != null && inputs['recover-version'] != '' }}
141+
id: already-complete
142+
run: |
143+
set -euo pipefail
144+
version="${{ inputs['recover-version'] }}"
145+
echo "Checking if v${version} is already fully published..."
146+
missing_reasons=""
147+
if ! release_assets_json="$(gh release view "v${version}" --json assets 2>/dev/null)"; then
148+
missing_reasons="github_release_missing"
149+
else
150+
missing_reasons="$(RELEASE_ASSETS_JSON="$release_assets_json" node scripts/release/check-release-complete.mjs "$version")"
151+
fi
152+
153+
if [[ "$missing_reasons" == "github_release_missing" ]]; then
154+
npm_check_output="$(node scripts/release/check-release-complete.mjs "$version")"
155+
if [[ -n "$npm_check_output" ]]; then
156+
missing_reasons="${missing_reasons};${npm_check_output}"
157+
fi
158+
fi
159+
160+
if [[ -z "$missing_reasons" ]]; then
161+
echo "Version v${version} is already fully published."
162+
echo "is_complete=true" >> "$GITHUB_OUTPUT"
163+
else
164+
echo "Version v${version} is not yet complete (${missing_reasons}). Proceeding with recovery."
165+
echo "is_complete=false" >> "$GITHUB_OUTPUT"
166+
fi
167+
168+
- name: Report already-complete recovery
169+
if: ${{ steps.already-complete.outputs.is_complete == 'true' }}
170+
run: |
171+
version="${{ inputs['recover-version'] }}"
172+
mkdir -p ../.cli-forge
173+
echo "is_recovery: true" > ../.cli-forge/release-receipt.yml
174+
echo "recovery_version: \"${version}\"" >> ../.cli-forge/release-receipt.yml
175+
echo "outcome: already_complete" >> ../.cli-forge/release-receipt.yml
176+
177+
- name: Build release binaries
178+
if: ${{ inputs['recover-version'] != null && inputs['recover-version'] != '' && (inputs['recover-run-id'] == null || inputs['recover-run-id'] == '') && steps.already-complete.outputs.is_complete != 'true' }}
179+
run: |
180+
echo "::warning::Rebuilding from source. Provide recover-run-id for byte-for-byte original artifacts."
181+
node scripts/release/build-binaries.mjs "${{ inputs['recover-version'] }}"
182+
183+
- name: Recover wedged release
184+
if: ${{ inputs['recover-version'] != null && inputs['recover-version'] != '' && steps.already-complete.outputs.is_complete != 'true' }}
185+
run: |
186+
set -euo pipefail
187+
version="${{ inputs['recover-version'] }}"
188+
mkdir -p ../.cli-forge
189+
echo "is_recovery: true" > ../.cli-forge/release-receipt.yml
190+
echo "recovery_version: \"${version}\"" >> ../.cli-forge/release-receipt.yml
191+
if [[ -z "${{ inputs['recover-run-id'] }}" ]]; then
192+
if gh release view "v${version}" --json assets -q '.assets | length' 2>/dev/null | grep -qv '^0$'; then
193+
echo "::error::GitHub Release v${version} already has assets. Provide recover-run-id to reuse original artifacts." >&2
194+
exit 1
195+
fi
196+
node --input-type=module -e "
197+
import { spawnSync } from 'node:child_process';
198+
import { buildAllPackageNames, readReleaseConfig } from './scripts/release/release-config.mjs';
199+
const v = '${version}';
200+
const names = buildAllPackageNames(readReleaseConfig(process.cwd()));
201+
for (const name of names) {
202+
const r = spawnSync('npm', ['view', name + '@' + v, 'version'], { encoding: 'utf8', stdio: 'pipe' });
203+
if (r.status === 0) {
204+
console.error(name + '@' + v + ' already published. Provide recover-run-id to reuse original artifacts.');
205+
process.exit(1);
206+
}
207+
const stderr = (r.stderr ?? '').toLowerCase();
208+
const notFound = stderr.includes('e404') || stderr.includes('etarget') || stderr.includes('not found');
209+
if (!notFound) throw new Error('npm view ' + name + '@' + v + ' failed: ' + (r.stderr ?? '').trim());
210+
}
211+
"
212+
fi
213+
node scripts/release/sync-platform-packages.mjs "$version"
214+
npm_output="$(node scripts/release/publish-npm-packages.mjs "$version" 2>&1)"
215+
echo "$npm_output"
216+
if echo "$npm_output" | grep -q '^skipped_steps:'; then
217+
echo "" >> ../.cli-forge/release-receipt.yml
218+
echo "$npm_output" | sed -n '/^skipped_steps:/,$p' >> ../.cli-forge/release-receipt.yml
219+
fi
220+
gh release view "v${version}" >/dev/null 2>&1 || gh release create "v${version}" --title "v${version}" --notes "Recovered release v${version}."
221+
existing=$(gh release view "v${version}" --json assets -q '.assets[].name' 2>/dev/null || true)
222+
upload_files=""
223+
for f in dist/*.tar.gz dist/*.sha256 dist/provenance.json; do
224+
[ -f "$f" ] || continue
225+
bn=$(basename "$f")
226+
if ! echo "$existing" | grep -qxF "$bn"; then
227+
upload_files="$upload_files $f"
228+
else
229+
echo "skip upload ${bn} (already on release)"
230+
fi
231+
done
232+
if [ -n "$upload_files" ]; then
233+
# shellcheck disable=SC2086
234+
gh release upload "v${version}" $upload_files
235+
fi
236+
237+
- name: Run semantic-release
238+
if: ${{ inputs['recover-version'] == null || inputs['recover-version'] == '' }}
239+
run: npx --yes semantic-release
240+
env:
241+
GIT_AUTHOR_NAME: github-actions[bot]
242+
GIT_AUTHOR_EMAIL: github-actions[bot]@users.noreply.github.com
243+
GIT_COMMITTER_NAME: github-actions[bot]
244+
GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com
245+
246+
- name: Preserve build artifacts
247+
if: always()
248+
uses: actions/upload-artifact@v4
249+
with:
250+
name: release-dist
251+
path: spec-forge-cli/dist/
252+
retention-days: 30

.gitignore

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
spec-forge-cli/target/
2+
spec-forge-cli/dist/
3+
spec-forge-cli/node_modules/
4+
spec-forge-cli/npm/platforms/*
5+
!spec-forge-cli/npm/platforms/.gitkeep
6+
.spec-forge/
7+
**/__pycache__/
8+
*.pyc
9+
.DS_Store
10+
*.log
11+
*.tmp

AGENTS.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<!-- SPECKIT START -->
2+
For additional context about technologies to be used, project structure,
3+
shell commands, and other important information, read [specs/001-spec-execution-stage/plan.md](specs/001-spec-execution-stage/plan.md).
4+
<!-- SPECKIT END -->

0 commit comments

Comments
 (0)