Skip to content

Commit 21138f6

Browse files
authored
Merge pull request #54 from BitConcepts/develop
release: v0.2.1
2 parents 6f72adc + 1bf12cf commit 21138f6

20 files changed

Lines changed: 895 additions & 60 deletions

.github/workflows/dev-release.yml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,22 @@ permissions:
99
contents: read
1010

1111
jobs:
12+
test:
13+
runs-on: ubuntu-latest
14+
steps:
15+
- uses: actions/checkout@v6
16+
- uses: actions/setup-python@v6
17+
with:
18+
python-version: "3.12"
19+
cache: pip
20+
- run: pip install -e ".[dev]"
21+
- run: ruff check src/ tests/
22+
- run: ruff format --check src/ tests/
23+
- run: mypy src/specsmith --ignore-missing-imports
24+
- run: pytest tests/ -x -q
25+
1226
dev-build:
27+
needs: test
1328
runs-on: ubuntu-latest
1429
steps:
1530
- uses: actions/checkout@v6

.github/workflows/release.yml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ jobs:
1818
python-version: "3.12"
1919
cache: pip
2020
- run: pip install -e ".[dev]"
21-
- run: ruff check src/
21+
- run: ruff check src/ tests/
22+
- run: ruff format --check src/ tests/
2223
- run: mypy src/specsmith --ignore-missing-imports
2324
- run: pytest tests/ -x -q
2425

@@ -54,7 +55,8 @@ jobs:
5455
run: |
5556
gh release create "${{ github.ref_name }}" dist/* \
5657
--title "${{ github.ref_name }}" \
57-
--generate-notes
58+
--generate-notes \
59+
--latest || echo "Release already exists — skipping"
5860
5961
pypi-publish:
6062
needs: build

CHANGELOG.md

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [0.2.1] - 2026-04-02
11+
12+
### Added
13+
- **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/`.
14+
- **`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.
15+
- **Language-specific scaffold templates** (#41): Rust (Cargo.toml, main.rs), Go (go.mod, main.go), JS/TS (package.json for web-frontend, fullstack-js).
16+
- **ReadTheDocs templates** (#38): `.readthedocs.yaml` and `mkdocs.yml` for Python/doc projects.
17+
- **Release workflow templates** (#44): `.github/workflows/release.yml` with test gate, language-aware build, GitHub Release, PyPI OIDC publish.
18+
- **PyPI integration** (#36): OIDC-based trusted publishing via release workflow template.
19+
20+
### Changed
21+
- **Template directory restructured** (#45): `pyproject.toml.j2` moved to `python/`. Templates organized into `python/`, `rust/`, `go/`, `js/`, `community/`, `governance/`, `docs/`, `scripts/`, `workflows/`.
22+
- **CI-gated releases**: both dev-release and stable release workflows now run full test suite (ruff check+format, mypy, pytest) before PyPI publish.
23+
- Exec shims (`exec.cmd`, `exec.sh`) now write PID files for `specsmith ps`/`specsmith abort`.
24+
1025
## [0.2.0] - 2026-04-02
1126

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

174-
[Unreleased]: https://github.com/BitConcepts/specsmith/compare/v0.2.0...HEAD
189+
[Unreleased]: https://github.com/BitConcepts/specsmith/compare/v0.2.1...HEAD
190+
[0.2.1]: https://github.com/BitConcepts/specsmith/compare/v0.2.0...v0.2.1
175191
[0.2.0]: https://github.com/BitConcepts/specsmith/compare/v0.1.3...v0.2.0
176192
[0.1.3]: https://github.com/BitConcepts/specsmith/compare/v0.1.2...v0.1.3
177193
[0.1.2]: https://github.com/BitConcepts/specsmith/compare/v0.1.1...v0.1.2

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "specsmith"
7-
version = "0.2.0"
7+
version = "0.2.1"
88
description = "Forge governed project scaffolds from the Agentic AI Development Workflow Specification."
99
readme = "README.md"
1010
license = {text = "MIT"}

src/specsmith/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,4 @@
88
try:
99
__version__: str = _pkg_version("specsmith")
1010
except PackageNotFoundError: # running from source without install
11-
__version__ = "0.2.0"
11+
__version__ = "0.2.1"

src/specsmith/cli.py

Lines changed: 101 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -291,12 +291,23 @@ def compress(project_dir: str, threshold: int, keep_recent: int) -> None:
291291
default=".",
292292
help="Project root directory.",
293293
)
294-
def upgrade(spec_version: str | None, project_dir: str) -> None:
295-
"""Update governance files to match a newer spec version."""
294+
@click.option(
295+
"--full",
296+
is_flag=True,
297+
default=False,
298+
help="Full sync: also regenerate exec shims, CI, agent files, create missing community files.",
299+
)
300+
def upgrade(spec_version: str | None, project_dir: str, full: bool) -> None:
301+
"""Update governance files to match a newer spec version.
302+
303+
With --full: also regenerates exec shims (PID tracking), CI configs,
304+
agent integrations, and creates missing community files. Safe: never
305+
overwrites AGENTS.md, LEDGER.md, or user documentation.
306+
"""
296307
from specsmith.upgrader import run_upgrade
297308

298309
root = Path(project_dir).resolve()
299-
result = run_upgrade(root, target_version=spec_version)
310+
result = run_upgrade(root, target_version=spec_version, full=full)
300311
console.print(result.message)
301312

302313
if result.updated_files:
@@ -1544,5 +1555,92 @@ def serve(port: int) -> None:
15441555
)
15451556

15461557

1558+
# ---------------------------------------------------------------------------
1559+
# Process execution and abort
1560+
# ---------------------------------------------------------------------------
1561+
1562+
1563+
@main.command(name="exec")
1564+
@click.argument("command")
1565+
@click.option("--timeout", default=120, help="Timeout in seconds (default: 120).")
1566+
@click.option("--project-dir", type=click.Path(exists=True), default=".")
1567+
def exec_cmd(command: str, timeout: int, project_dir: str) -> None:
1568+
"""Execute a command with PID tracking and timeout enforcement.
1569+
1570+
Tracks the process in .specsmith/pids/ so it can be listed (specsmith ps)
1571+
or aborted (specsmith abort). Logs stdout/stderr to .specsmith/logs/.
1572+
Works cross-platform: Windows, Linux, macOS.
1573+
"""
1574+
from specsmith.executor import run_tracked
1575+
1576+
root = Path(project_dir).resolve()
1577+
console.print(f"[bold]exec[/bold] {command} (timeout={timeout}s)")
1578+
1579+
result = run_tracked(root, command, timeout=timeout)
1580+
1581+
if result.timed_out:
1582+
console.print(f"[red]TIMEOUT[/red] after {timeout}s (PID {result.pid})")
1583+
elif result.exit_code == 0:
1584+
console.print(f"[green]OK[/green] ({result.duration:.1f}s) — exit code 0")
1585+
else:
1586+
console.print(f"[red]FAILED[/red] ({result.duration:.1f}s) — exit code {result.exit_code}")
1587+
if result.stdout_file:
1588+
console.print(f" stdout: {result.stdout_file}")
1589+
if result.stderr_file:
1590+
console.print(f" stderr: {result.stderr_file}")
1591+
raise SystemExit(result.exit_code)
1592+
1593+
1594+
@main.command(name="ps")
1595+
@click.option("--project-dir", type=click.Path(exists=True), default=".")
1596+
def ps_cmd(project_dir: str) -> None:
1597+
"""List tracked running processes."""
1598+
from specsmith.executor import list_processes
1599+
1600+
root = Path(project_dir).resolve()
1601+
procs = list_processes(root)
1602+
if not procs:
1603+
console.print("No tracked processes running.")
1604+
return
1605+
for p in procs:
1606+
elapsed = p.elapsed
1607+
remaining = max(0, p.timeout - elapsed)
1608+
status = "[red]EXPIRED[/red]" if p.is_expired else f"{remaining:.0f}s left"
1609+
console.print(f" PID {p.pid} {status} {p.command}")
1610+
console.print(f"\n {len(procs)} process(es)")
1611+
1612+
1613+
@main.command(name="abort")
1614+
@click.option("--pid", type=int, default=None, help="Abort a specific PID.")
1615+
@click.option("--all", "abort_all_flag", is_flag=True, default=False, help="Abort all tracked.")
1616+
@click.option("--project-dir", type=click.Path(exists=True), default=".")
1617+
def abort_cmd(pid: int | None, abort_all_flag: bool, project_dir: str) -> None:
1618+
"""Abort tracked process(es). Sends SIGTERM then SIGKILL (POSIX) or taskkill (Windows)."""
1619+
from specsmith.executor import abort_all, abort_process, list_processes
1620+
1621+
root = Path(project_dir).resolve()
1622+
1623+
if abort_all_flag:
1624+
killed = abort_all(root)
1625+
if killed:
1626+
console.print(f"[green]Aborted {len(killed)} process(es): {killed}[/green]")
1627+
else:
1628+
console.print("No tracked processes to abort.")
1629+
elif pid:
1630+
if abort_process(root, pid):
1631+
console.print(f"[green]Aborted PID {pid}[/green]")
1632+
else:
1633+
console.print(f"[red]Could not abort PID {pid}[/red]")
1634+
else:
1635+
procs = list_processes(root)
1636+
if not procs:
1637+
console.print("No tracked processes. Use --pid or --all.")
1638+
return
1639+
console.print("Tracked processes:")
1640+
for p in procs:
1641+
console.print(f" PID {p.pid} {p.command}")
1642+
console.print("\nUse --pid <N> or --all to abort.")
1643+
1644+
15471645
if __name__ == "__main__":
15481646
main()

0 commit comments

Comments
 (0)