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
15 changes: 15 additions & 0 deletions .github/workflows/dev-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,22 @@ permissions:
contents: read

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: "3.12"
cache: pip
- run: pip install -e ".[dev]"
- run: ruff check src/ tests/
- run: ruff format --check src/ tests/
- run: mypy src/specsmith --ignore-missing-imports
- run: pytest tests/ -x -q

dev-build:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
Expand Down
6 changes: 4 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ jobs:
python-version: "3.12"
cache: pip
- run: pip install -e ".[dev]"
- run: ruff check src/
- run: ruff check src/ tests/
- run: ruff format --check src/ tests/
- run: mypy src/specsmith --ignore-missing-imports
- run: pytest tests/ -x -q

Expand Down Expand Up @@ -54,7 +55,8 @@ jobs:
run: |
gh release create "${{ github.ref_name }}" dist/* \
--title "${{ github.ref_name }}" \
--generate-notes
--generate-notes \
--latest || echo "Release already exists — skipping"

pypi-publish:
needs: build
Expand Down
18 changes: 17 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.2.1] - 2026-04-02

### Added
- **Process execution with PID tracking**: `specsmith exec`, `specsmith ps`, `specsmith abort` — cross-platform (Windows taskkill / POSIX SIGTERM+SIGKILL) process tracking and abort. PID files in `.specsmith/pids/`.
- **`specsmith upgrade --full`**: full sync of infrastructure files — regenerates exec shims, CI configs, agent integrations. Creates missing community/config files. Safe: never overwrites user docs.
- **Language-specific scaffold templates** (#41): Rust (Cargo.toml, main.rs), Go (go.mod, main.go), JS/TS (package.json for web-frontend, fullstack-js).
- **ReadTheDocs templates** (#38): `.readthedocs.yaml` and `mkdocs.yml` for Python/doc projects.
- **Release workflow templates** (#44): `.github/workflows/release.yml` with test gate, language-aware build, GitHub Release, PyPI OIDC publish.
- **PyPI integration** (#36): OIDC-based trusted publishing via release workflow template.

### Changed
- **Template directory restructured** (#45): `pyproject.toml.j2` moved to `python/`. Templates organized into `python/`, `rust/`, `go/`, `js/`, `community/`, `governance/`, `docs/`, `scripts/`, `workflows/`.
- **CI-gated releases**: both dev-release and stable release workflows now run full test suite (ruff check+format, mypy, pytest) before PyPI publish.
- Exec shims (`exec.cmd`, `exec.sh`) now write PID files for `specsmith ps`/`specsmith abort`.

## [0.2.0] - 2026-04-02

### Added
Expand Down Expand Up @@ -171,7 +186,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **G9**: Session start file list now marks services.md as conditional ("if it exists").
- **G10**: Open TODOs format specified as `- [ ]` / `- [x]` checkbox syntax.

[Unreleased]: https://github.com/BitConcepts/specsmith/compare/v0.2.0...HEAD
[Unreleased]: https://github.com/BitConcepts/specsmith/compare/v0.2.1...HEAD
[0.2.1]: https://github.com/BitConcepts/specsmith/compare/v0.2.0...v0.2.1
[0.2.0]: https://github.com/BitConcepts/specsmith/compare/v0.1.3...v0.2.0
[0.1.3]: https://github.com/BitConcepts/specsmith/compare/v0.1.2...v0.1.3
[0.1.2]: https://github.com/BitConcepts/specsmith/compare/v0.1.1...v0.1.2
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "specsmith"
version = "0.2.0"
version = "0.2.1"
description = "Forge governed project scaffolds from the Agentic AI Development Workflow Specification."
readme = "README.md"
license = {text = "MIT"}
Expand Down
2 changes: 1 addition & 1 deletion src/specsmith/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@
try:
__version__: str = _pkg_version("specsmith")
except PackageNotFoundError: # running from source without install
__version__ = "0.2.0"
__version__ = "0.2.1"
104 changes: 101 additions & 3 deletions src/specsmith/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -291,12 +291,23 @@ def compress(project_dir: str, threshold: int, keep_recent: int) -> None:
default=".",
help="Project root directory.",
)
def upgrade(spec_version: str | None, project_dir: str) -> None:
"""Update governance files to match a newer spec version."""
@click.option(
"--full",
is_flag=True,
default=False,
help="Full sync: also regenerate exec shims, CI, agent files, create missing community files.",
)
def upgrade(spec_version: str | None, project_dir: str, full: bool) -> None:
"""Update governance files to match a newer spec version.

With --full: also regenerates exec shims (PID tracking), CI configs,
agent integrations, and creates missing community files. Safe: never
overwrites AGENTS.md, LEDGER.md, or user documentation.
"""
from specsmith.upgrader import run_upgrade

root = Path(project_dir).resolve()
result = run_upgrade(root, target_version=spec_version)
result = run_upgrade(root, target_version=spec_version, full=full)
console.print(result.message)

if result.updated_files:
Expand Down Expand Up @@ -1544,5 +1555,92 @@ def serve(port: int) -> None:
)


# ---------------------------------------------------------------------------
# Process execution and abort
# ---------------------------------------------------------------------------


@main.command(name="exec")
@click.argument("command")
@click.option("--timeout", default=120, help="Timeout in seconds (default: 120).")
@click.option("--project-dir", type=click.Path(exists=True), default=".")
def exec_cmd(command: str, timeout: int, project_dir: str) -> None:
"""Execute a command with PID tracking and timeout enforcement.

Tracks the process in .specsmith/pids/ so it can be listed (specsmith ps)
or aborted (specsmith abort). Logs stdout/stderr to .specsmith/logs/.
Works cross-platform: Windows, Linux, macOS.
"""
from specsmith.executor import run_tracked

root = Path(project_dir).resolve()
console.print(f"[bold]exec[/bold] {command} (timeout={timeout}s)")

result = run_tracked(root, command, timeout=timeout)

if result.timed_out:
console.print(f"[red]TIMEOUT[/red] after {timeout}s (PID {result.pid})")
elif result.exit_code == 0:
console.print(f"[green]OK[/green] ({result.duration:.1f}s) — exit code 0")
else:
console.print(f"[red]FAILED[/red] ({result.duration:.1f}s) — exit code {result.exit_code}")
if result.stdout_file:
console.print(f" stdout: {result.stdout_file}")
if result.stderr_file:
console.print(f" stderr: {result.stderr_file}")
raise SystemExit(result.exit_code)


@main.command(name="ps")
@click.option("--project-dir", type=click.Path(exists=True), default=".")
def ps_cmd(project_dir: str) -> None:
"""List tracked running processes."""
from specsmith.executor import list_processes

root = Path(project_dir).resolve()
procs = list_processes(root)
if not procs:
console.print("No tracked processes running.")
return
for p in procs:
elapsed = p.elapsed
remaining = max(0, p.timeout - elapsed)
status = "[red]EXPIRED[/red]" if p.is_expired else f"{remaining:.0f}s left"
console.print(f" PID {p.pid} {status} {p.command}")
console.print(f"\n {len(procs)} process(es)")


@main.command(name="abort")
@click.option("--pid", type=int, default=None, help="Abort a specific PID.")
@click.option("--all", "abort_all_flag", is_flag=True, default=False, help="Abort all tracked.")
@click.option("--project-dir", type=click.Path(exists=True), default=".")
def abort_cmd(pid: int | None, abort_all_flag: bool, project_dir: str) -> None:
"""Abort tracked process(es). Sends SIGTERM then SIGKILL (POSIX) or taskkill (Windows)."""
from specsmith.executor import abort_all, abort_process, list_processes

root = Path(project_dir).resolve()

if abort_all_flag:
killed = abort_all(root)
if killed:
console.print(f"[green]Aborted {len(killed)} process(es): {killed}[/green]")
else:
console.print("No tracked processes to abort.")
elif pid:
if abort_process(root, pid):
console.print(f"[green]Aborted PID {pid}[/green]")
else:
console.print(f"[red]Could not abort PID {pid}[/red]")
else:
procs = list_processes(root)
if not procs:
console.print("No tracked processes. Use --pid or --all.")
return
console.print("Tracked processes:")
for p in procs:
console.print(f" PID {p.pid} {p.command}")
console.print("\nUse --pid <N> or --all to abort.")


if __name__ == "__main__":
main()
Loading
Loading