From 73074f903ca717540c417fefecbf9b34b4c3476d Mon Sep 17 00:00:00 2001 From: Michael Wu Date: Wed, 3 Jun 2026 01:45:14 +0800 Subject: [PATCH] Restructure devkit stacks and feedback templates --- .context/architecture/repository-layout.md | 12 - .context/decisions/tooling.md | 34 -- .context/failures/.gitkeep | 1 - .context/failures/README.md | 5 - .context/runbooks/local-dev.md | 11 - .context/summaries/.gitkeep | 1 - .context/summaries/README.md | 5 - .dockerignore | 1 + .env.example | 24 +- .github/PULL_REQUEST_TEMPLATE.md | 2 +- .github/workflows/ci.yml | 111 ++++--- .gitignore | 4 +- .pre-commit-config.yaml | 17 +- AGENTS.md | 46 +-- CLAUDE.md | 5 +- CONTRIBUTING.md | 11 +- DECISIONS.md | 20 +- README.md | 102 ++++-- SECURITY.md | 15 +- alternates/dev-scripts/README.md | 17 - alternates/devcontainer/README.md | 7 - alternates/github/README.md | 13 - alternates/pnpm/README.md | 22 -- alternates/pnpm/package.json | 26 -- apps/web/src/index.ts | 3 - biome.json | 2 +- bun.lock | 4 +- compose.yml | 24 +- docs/agent-walkthrough.md | 14 +- docs/deployment.md | 9 + docs/development.md | 37 ++- docs/frontend.md | 9 +- docs/github-workflows.md | 44 ++- docs/interfaces.md | 8 + docs/observability.md | 8 + docs/pattern-report.md | 31 +- docs/secrets.md | 14 + docs/supply-chain.md | 54 ++- docs/template-proposal.md | 30 +- docs/tooling.md | 70 ++++ extras/dev-scripts/README.md | 26 ++ {alternates => extras}/dev-scripts/dev.ts | 2 +- .../dev-scripts/worktree-ports.mjs | 2 +- extras/devcontainer/README.md | 13 + .../devcontainer/devcontainer.json.example | 0 .../dockerfiles/Dockerfile.api.example | 0 .../Dockerfile.web-typescript.example | 4 +- .../dockerfiles/Dockerfile.worker.example | 0 {alternates => extras}/dockerfiles/README.md | 8 + .../github/CODEOWNERS.example | 0 extras/github/README.md | 37 +++ .../DISCUSSION_TEMPLATE/questions.yml | 0 extras/github/dependency-review.yml.example | 26 ++ .../github/gitleaks.yml.example | 27 +- extras/object-storage/README.md | 43 +++ .../compose.object-storage.yml.example | 36 ++ .../todo-to-issue/README.md | 9 +- .../todo-to-issue/todo-to-issue.yml.example | 0 llms.txt | 15 +- package.json | 18 +- pnpm-workspace.example.yaml | 6 +- scripts/check-all.sh | 5 +- scripts/dev.sh | 62 +++- scripts/docker-compose.sh | 32 +- scripts/format.sh | 4 +- scripts/lint.sh | 5 +- scripts/test.sh | 4 +- scripts/typecheck.sh | 4 +- scripts/worktree-ports.sh | 309 ++++++++++++++++++ skills/508-devkit/SKILL.md | 109 ++++-- skills/add-migration/SKILL.md | 3 +- stacks/python/README.md | 64 ++++ {apps => stacks/python/apps}/api/alembic.ini | 0 .../python/apps}/api/migrations/env.py | 0 .../apps}/api/migrations/script.py.mako | 0 .../python/apps}/api/pyproject.toml | 0 .../apps}/api/src/example_api/__init__.py | 0 .../python/apps}/api/src/example_api/db.py | 1 + .../python/apps}/api/src/example_api/main.py | 2 + .../python/apps}/api/tests/test_health.py | 0 .../api/tests/test_postgres_integration.py | 0 .../python/packages}/shared/pyproject.toml | 0 .../shared/src/example_shared/__init__.py | 0 .../src/example_shared/observability.py | 0 .../shared/src/example_shared/schemas.py | 0 .../shared/src/example_shared/settings.py | 5 + .../python/pyproject.toml | 4 - stacks/python/scripts/check-all.sh | 10 + stacks/python/scripts/dev.sh | 24 ++ stacks/python/scripts/format.sh | 7 + stacks/python/scripts/lint.sh | 7 + stacks/python/scripts/test.sh | 7 + stacks/python/scripts/typecheck.sh | 7 + .../python/scripts}/worktree-ports.py | 121 ++++++- stacks/python/tests/test_worktree_ports.py | 94 ++++++ uv.lock => stacks/python/uv.lock | 4 - stacks/typescript/README.md | 28 ++ .../typescript}/drizzle.config.ts | 0 {apps/web => stacks/typescript}/package.json | 0 stacks/typescript/pnpm/README.md | 40 +++ .../typescript}/pnpm/ci-web-job.yml | 8 +- stacks/typescript/pnpm/package.json | 27 ++ .../typescript}/pnpm/pnpm-workspace.yaml | 2 +- .../typescript}/src/db/schema.ts | 2 + stacks/typescript/src/index.ts | 5 + .../typescript}/tests/index.test.ts | 0 {apps/web => stacks/typescript}/tsconfig.json | 0 .../typescript}/vitest.config.ts | 0 tests/test_worktree_ports.py | 41 --- 109 files changed, 1671 insertions(+), 521 deletions(-) delete mode 100644 .context/architecture/repository-layout.md delete mode 100644 .context/decisions/tooling.md delete mode 100644 .context/failures/.gitkeep delete mode 100644 .context/failures/README.md delete mode 100644 .context/runbooks/local-dev.md delete mode 100644 .context/summaries/.gitkeep delete mode 100644 .context/summaries/README.md delete mode 100644 alternates/dev-scripts/README.md delete mode 100644 alternates/devcontainer/README.md delete mode 100644 alternates/github/README.md delete mode 100644 alternates/pnpm/README.md delete mode 100644 alternates/pnpm/package.json delete mode 100644 apps/web/src/index.ts create mode 100644 docs/tooling.md create mode 100644 extras/dev-scripts/README.md rename {alternates => extras}/dev-scripts/dev.ts (92%) rename {alternates => extras}/dev-scripts/worktree-ports.mjs (100%) create mode 100644 extras/devcontainer/README.md rename {alternates => extras}/devcontainer/devcontainer.json.example (100%) rename {alternates => extras}/dockerfiles/Dockerfile.api.example (100%) rename {alternates => extras}/dockerfiles/Dockerfile.web-typescript.example (61%) rename {alternates => extras}/dockerfiles/Dockerfile.worker.example (100%) rename {alternates => extras}/dockerfiles/README.md (65%) rename {alternates => extras}/github/CODEOWNERS.example (100%) create mode 100644 extras/github/README.md rename {alternates => extras}/github/community/DISCUSSION_TEMPLATE/questions.yml (100%) create mode 100644 extras/github/dependency-review.yml.example rename .github/workflows/security.yml => extras/github/gitleaks.yml.example (60%) create mode 100644 extras/object-storage/README.md create mode 100644 extras/object-storage/compose.object-storage.yml.example rename {alternates => extras}/todo-to-issue/README.md (69%) rename {alternates => extras}/todo-to-issue/todo-to-issue.yml.example (100%) create mode 100755 scripts/worktree-ports.sh create mode 100644 stacks/python/README.md rename {apps => stacks/python/apps}/api/alembic.ini (100%) rename {apps => stacks/python/apps}/api/migrations/env.py (100%) rename {apps => stacks/python/apps}/api/migrations/script.py.mako (100%) rename {apps => stacks/python/apps}/api/pyproject.toml (100%) rename {apps => stacks/python/apps}/api/src/example_api/__init__.py (100%) rename {apps => stacks/python/apps}/api/src/example_api/db.py (70%) rename {apps => stacks/python/apps}/api/src/example_api/main.py (84%) rename {apps => stacks/python/apps}/api/tests/test_health.py (100%) rename {apps => stacks/python/apps}/api/tests/test_postgres_integration.py (100%) rename {packages => stacks/python/packages}/shared/pyproject.toml (100%) rename {packages => stacks/python/packages}/shared/src/example_shared/__init__.py (100%) rename {packages => stacks/python/packages}/shared/src/example_shared/observability.py (100%) rename {packages => stacks/python/packages}/shared/src/example_shared/schemas.py (100%) rename {packages => stacks/python/packages}/shared/src/example_shared/settings.py (74%) rename pyproject.toml => stacks/python/pyproject.toml (96%) create mode 100755 stacks/python/scripts/check-all.sh create mode 100755 stacks/python/scripts/dev.sh create mode 100755 stacks/python/scripts/format.sh create mode 100755 stacks/python/scripts/lint.sh create mode 100755 stacks/python/scripts/test.sh create mode 100755 stacks/python/scripts/typecheck.sh rename {scripts => stacks/python/scripts}/worktree-ports.py (51%) create mode 100644 stacks/python/tests/test_worktree_ports.py rename uv.lock => stacks/python/uv.lock (99%) create mode 100644 stacks/typescript/README.md rename {apps/web => stacks/typescript}/drizzle.config.ts (100%) rename {apps/web => stacks/typescript}/package.json (100%) create mode 100644 stacks/typescript/pnpm/README.md rename {alternates => stacks/typescript}/pnpm/ci-web-job.yml (62%) create mode 100644 stacks/typescript/pnpm/package.json rename {alternates => stacks/typescript}/pnpm/pnpm-workspace.yaml (87%) rename {apps/web => stacks/typescript}/src/db/schema.ts (64%) create mode 100644 stacks/typescript/src/index.ts rename {apps/web => stacks/typescript}/tests/index.test.ts (100%) rename {apps/web => stacks/typescript}/tsconfig.json (100%) rename {apps/web => stacks/typescript}/vitest.config.ts (100%) delete mode 100644 tests/test_worktree_ports.py diff --git a/.context/architecture/repository-layout.md b/.context/architecture/repository-layout.md deleted file mode 100644 index 759ba9b..0000000 --- a/.context/architecture/repository-layout.md +++ /dev/null @@ -1,12 +0,0 @@ -# Repository Layout - -This repository uses an app/package split: - -- `apps/api`: minimal Python app/shared-package wiring. -- `apps/web`: framework-neutral TypeScript conventions. -- `packages/shared`: shared settings, clients, schemas, queue helpers, and pure business logic. -- `scripts`: deterministic project entrypoints used by humans, CI, and agents. -- `docs`: durable user-facing or contributor-facing documentation. -- `.context`: operational memory for humans and agents. - -Treat `apps/*` here as reference wiring, not product code. Keep service-specific behavior in its service when applying the devkit to a real repo. Put shared contracts and pure helpers in `packages/shared`. diff --git a/.context/decisions/tooling.md b/.context/decisions/tooling.md deleted file mode 100644 index e2f1ad1..0000000 --- a/.context/decisions/tooling.md +++ /dev/null @@ -1,34 +0,0 @@ -# Tooling Decisions - -## Python - -Use `uv` for installs and execution. Keep Python configuration in `pyproject.toml`. - -Required checks: - -- `uv run ruff check` -- `uv run ruff format --check` -- `uv run mypy` -- `uv run pytest` - -## JavaScript and TypeScript - -Use Bun for new projects. It matches the preferred default for greenfield work and keeps scripts fast and direct. - -Use pnpm only when a workspace grows large enough that its monorepo tooling, workspace controls, or ecosystem compatibility clearly outweigh the simplicity of Bun. - -Required checks: - -- `bun run lint` -- `bun run format:check` -- `bun run typecheck` -- `bun run test` - -## Dependency Safety - -Use dependency cooldowns and frozen installs: - -- `uv`: `exclude-newer = "7 days"`. -- Bun: `bunfig.toml` sets `minimumReleaseAge = 604800` seconds. -- pnpm: `pnpm-workspace.yaml` should set `minimumReleaseAge: 10080` minutes. -- CI should use locked or frozen installs. diff --git a/.context/failures/.gitkeep b/.context/failures/.gitkeep deleted file mode 100644 index 8b13789..0000000 --- a/.context/failures/.gitkeep +++ /dev/null @@ -1 +0,0 @@ - diff --git a/.context/failures/README.md b/.context/failures/README.md deleted file mode 100644 index b445f3f..0000000 --- a/.context/failures/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Failures - -Record confirmed failed approaches, recurring error patterns, and known bad paths. - -Keep entries concise and include enough context for future agents to avoid repeating the same work. diff --git a/.context/runbooks/local-dev.md b/.context/runbooks/local-dev.md deleted file mode 100644 index 15edbd2..0000000 --- a/.context/runbooks/local-dev.md +++ /dev/null @@ -1,11 +0,0 @@ -# Local Development Runbook - -1. Copy `.env.example` to `.env` and fill required secrets. -2. Run `./scripts/worktree-ports.py env` to inspect derived ports. -3. Start local infrastructure with `./scripts/docker-compose.sh up -d`. -4. Start host services with `./scripts/dev.sh`. - -When creating sibling worktrees, use `.worktreeinclude` to copy only ignored local config such as `.env` and `.sops.yaml`. Docker build contexts should use `.dockerignore` to exclude secrets, dependencies, caches, and `.context/` scratch state. -5. Run checks with `./scripts/check-all.sh`. - -App services should run on the host for reload speed and easier debugging. Docker Compose should own infrastructure such as Postgres, Redis, and optional object storage. diff --git a/.context/summaries/.gitkeep b/.context/summaries/.gitkeep deleted file mode 100644 index 8b13789..0000000 --- a/.context/summaries/.gitkeep +++ /dev/null @@ -1 +0,0 @@ - diff --git a/.context/summaries/README.md b/.context/summaries/README.md deleted file mode 100644 index 3e5078f..0000000 --- a/.context/summaries/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Summaries - -Record high-signal project evolution summaries and recurring lessons. - -Prefer short synthesized notes over raw transcripts, logs, or generated artifacts. diff --git a/.dockerignore b/.dockerignore index 2eb37ea..f80d53a 100644 --- a/.dockerignore +++ b/.dockerignore @@ -2,6 +2,7 @@ .github .context .cursor +.claude .env .env.* diff --git a/.env.example b/.env.example index 0d932ef..1a1b091 100644 --- a/.env.example +++ b/.env.example @@ -10,32 +10,32 @@ WEB_HOST=127.0.0.1 # WEB_PORT=8730 # WORKER_HEALTH_PORT=8735 -# Docker-published infra ports. Leave unset locally to use deterministic worktree ports. +# Optional worktree orchestrator reservations. Leave unset for path-hashed ports. +# Use WORKTREE_PORT_BLOCK_START when the orchestrator reserves a small range. +# Use WORKTREE_PRIMARY_PORT when the orchestrator reserves one public port. +# WORKTREE_PRIMARY_PORT_TARGET can be WEB_PORT or API_PORT. +# WORKTREE_PORT_BLOCK_START= +# WORKTREE_PORT_BLOCK_SIZE=10 +# WORKTREE_PRIMARY_PORT= +# WORKTREE_PRIMARY_PORT_TARGET=WEB_PORT + +# Docker-published example infra ports. Leave unset locally to use deterministic worktree ports. POSTGRES_HOST_BIND=127.0.0.1 # POSTGRES_HOST_PORT=8740 REDIS_HOST_BIND=127.0.0.1 # REDIS_HOST_PORT=8750 -MINIO_HOST_BIND=127.0.0.1 -# MINIO_API_HOST_PORT=8760 -# MINIO_CONSOLE_HOST_PORT=8761 -# Postgres +# Example database POSTGRES_DB=app POSTGRES_USER=app POSTGRES_PASSWORD=app POSTGRES_URL=postgresql://app:app@127.0.0.1:8740/app DATABASE_URL=postgresql://app:app@127.0.0.1:8740/app -# Redis +# Example cache/queue REDIS_URL=redis://127.0.0.1:8750/0 REDIS_QUEUE_NAME=jobs.default -# Optional object storage -MINIO_ENDPOINT=http://127.0.0.1:8760 -MINIO_ROOT_USER=internal -MINIO_ROOT_PASSWORD=change-me -MINIO_INTERNAL_BUCKET=internal-transfers - # Observability SENTRY_DSN= OTEL_EXPORTER_OTLP_ENDPOINT= diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 6796d76..e5a6951 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,6 +1,6 @@ ## Summary -- +- ## Validation diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 075abe9..f736ba6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,14 +13,13 @@ concurrency: permissions: contents: read - pull-requests: read jobs: changes: runs-on: ubuntu-latest outputs: + typescript: ${{ steps.filter.outputs.typescript }} python: ${{ steps.filter.outputs.python }} - web: ${{ steps.filter.outputs.web }} infra: ${{ steps.filter.outputs.infra }} workflow: ${{ steps.filter.outputs.workflow }} steps: @@ -30,27 +29,27 @@ jobs: - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 with: + # paths-filter runs with token: "" below, so it needs enough history + # to compare commits locally instead of calling the PR files API. fetch-depth: 0 persist-credentials: false - id: filter uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4 with: + # Keep the workflow at contents:read. Empty token forces git-based + # detection and avoids requiring pull-requests:read. + token: "" filters: | - python: - - "apps/api/**" - - "packages/shared/**" - - "tests/**" - - "pyproject.toml" - - "uv.lock" - - "scripts/*.sh" - - "scripts/*.py" - web: - - "apps/web/**" + typescript: + - "stacks/typescript/**" - "package.json" - "bun.lock" - "bunfig.toml" - "biome.json" + - "scripts/*.sh" + python: + - "stacks/python/**" infra: - "compose.yml" - "docker-compose.yml" @@ -58,9 +57,9 @@ jobs: workflow: - ".github/workflows/**" - python: + typescript: needs: changes - if: needs.changes.outputs.python == 'true' || needs.changes.outputs.workflow == 'true' + if: needs.changes.outputs.typescript == 'true' || needs.changes.outputs.workflow == 'true' runs-on: ubuntu-latest steps: - uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2 @@ -71,31 +70,28 @@ jobs: with: persist-credentials: false - - uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0 + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 with: - enable-cache: true - - - name: Install Python - run: uv python install 3.12 + bun-version: 1.3.13 - name: Install dependencies - run: uv sync --locked + run: bun install --frozen-lockfile - - name: Ruff - run: uv run ruff check apps packages tests + - name: Biome + run: bun run lint - - name: Ruff format - run: uv run ruff format --check apps packages tests + - name: Typecheck + run: bun run typecheck - - name: MyPy - run: uv run mypy + - name: Tests + run: bun run test - - name: Pytest - run: uv run pytest + - name: Build + run: bun run build - web: + python: needs: changes - if: needs.changes.outputs.web == 'true' || needs.changes.outputs.workflow == 'true' + if: needs.changes.outputs.python == 'true' || needs.changes.outputs.workflow == 'true' runs-on: ubuntu-latest steps: - uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2 @@ -106,24 +102,19 @@ jobs: with: persist-credentials: false - - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 + - uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0 with: - bun-version: 1.3.13 - - - name: Install dependencies - run: bun install --frozen-lockfile - - - name: Biome - run: bun run format:check && bun run lint + enable-cache: true - - name: Typecheck - run: bun run typecheck + - name: Install Python + run: uv python install 3.12 - - name: Tests - run: bun run test + - name: Install dependencies + working-directory: stacks/python + run: uv sync --locked - - name: Build - run: bun run build + - name: Check Python stack + run: ./stacks/python/scripts/check-all.sh compose: needs: changes @@ -140,3 +131,35 @@ jobs: - name: Validate Compose config run: docker compose -f compose.yml --env-file .env.example config + + ci-passed: + if: always() + needs: + - changes + - typescript + - python + - compose + runs-on: ubuntu-latest + steps: + - name: Check required jobs + run: | + changes_result='${{ needs.changes.result }}' + typescript_result='${{ needs.typescript.result }}' + python_result='${{ needs.python.result }}' + compose_result='${{ needs.compose.result }}' + if [ "$changes_result" != "success" ]; then + echo "The changes job did not pass: changes=${changes_result}" >&2 + exit 1 + fi + # Component jobs may be skipped by the path filter. That is healthy + # only when path detection itself succeeded, which is checked above. + for result in "$typescript_result" "$python_result" "$compose_result"; do + case "$result" in + success|skipped) + ;; + *) + echo "A required CI job did not pass: changes=${changes_result}, typescript=${typescript_result}, python=${python_result}, compose=${compose_result}" >&2 + exit 1 + ;; + esac + done diff --git a/.gitignore b/.gitignore index a878d7a..28e80db 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ .DS_Store .env .venv/ +.context/ +.claude/ __pycache__/ .mypy_cache/ .pytest_cache/ @@ -14,5 +16,3 @@ build/ .next/ .turbo/ *.log -.context/artifacts/ -.context/screenshots/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 14491e3..c283857 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,22 +1,11 @@ repos: - - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.14.0 - hooks: - - id: ruff - args: [--fix] - - id: ruff-format - - repo: local hooks: - - id: mypy - name: mypy - entry: ./scripts/typecheck.sh - language: system - pass_filenames: false - files: ^(apps/api/src|apps/worker/src|packages/shared/src)/ - id: biome name: biome entry: bun run format:check language: system pass_filenames: false - files: ^(apps/web/|packages/|scripts/|biome\.json$|package\.json$) + # Keep the hook scoped to root tooling and the TypeScript stack. Add + # stack-specific hooks only after selecting that stack for a target repo. + files: ^(stacks/typescript/|stacks/.*/package\.json$|scripts/|biome\.json$|package\.json$) diff --git a/AGENTS.md b/AGENTS.md index 68e684c..9d1a92b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -3,34 +3,35 @@ ## Environment - Only `python3` is guaranteed. Do not assume `python` exists. -- Prefer `uv run`, `bun run`, and scripts in `scripts/` over raw commands. +- Prefer package scripts and repo-provided entrypoints over raw commands. When a + repo uses Python, prefer `uv run`; when it uses Bun, prefer `bun run`. - Treat install, dev, and test commands as executable code. Inspect manifests, package scripts, lockfiles, Docker files, and setup scripts before running them in unfamiliar repos. ## Dependency Supply-Chain Safety - Bun: keep `bunfig.toml` with `minimumReleaseAge = 604800`. -- uv: keep `exclude-newer = "7 days"`. -- pnpm fallback: keep `minimumReleaseAge: 10080` in `pnpm-workspace.yaml`. +- uv: add optional `exclude-newer = "P7D"` only after confirming the local + `uv` version supports relative `exclude-newer` durations. +- pnpm: keep `minimumReleaseAge: 10080` in `pnpm-workspace.yaml`. - CI should use locked installs: - `bun install --frozen-lockfile` - - `uv sync --locked` + - `uv sync --locked` when a Python workspace is present - `pnpm install --frozen-lockfile` when pnpm is used. - Commit lockfiles. ## Repository Shape -- `apps/api`: minimal Python wiring example. -- `apps/web`: framework-neutral Bun/TypeScript workspace for web-side conventions. -- `packages/shared`: shared Python settings, schemas, and helpers. +- `stacks/typescript`: framework-neutral Bun/TypeScript conventions. - `scripts`: stable project entrypoints. - `docs`: contributor-facing documentation. -- `.context`: operational memory for humans and agents. +- `stacks/python`: optional Python API/shared-package workspace. +- `.context`: gitignored workspace-local scratch for Conductor and agents. ## Development Workflow - Run infrastructure with Docker Compose. - Run app services on the host for reload speed and debuggability. -- Use `./scripts/worktree-ports.py env` to inspect local ports. +- Use `./scripts/worktree-ports.sh env` to inspect local ports. - Use `./scripts/docker-compose.sh` instead of raw `docker compose` for local worktree-safe infra. - Use `./scripts/dev.sh` for host-run app services. - Treat `apps/*` as disposable wiring examples, not framework code to cargo-cult into every project. @@ -46,22 +47,29 @@ - Add or update tests when behavior changes. - Update `.env.example` when adding configuration. - Update docs when changing developer workflows. -- Use Pydantic for Python settings and boundary schemas. -- Use Alembic for Python database migrations. -- Use Drizzle for TypeScript database access. +- When selecting the Python stack, the included examples use Pydantic for + settings/boundary schemas and Alembic for database migrations. Keep them when + they fit; replace them when the target repo has better existing choices. +- Before adding uv cooldown config, run `uv --no-config --version`. Relative + `exclude-newer` values such as `P7D` require uv `0.9.17` or newer. If the + target machine is older, ask before upgrading uv; do not write `P7D` or + `7 days` into `pyproject.toml` or `uv.toml` because older uv clients fail + during settings discovery. +- The TypeScript stack includes Drizzle examples for database access. Keep + Drizzle when it fits; replace it when the target repo already uses another + data-access layer. - Keep secrets in environment variables or SOPS-managed files, never in code. ## `.context/` -Use `.context/` for concise operational memory: +Use `.context/` for workspace-local agent scratch only. Do not commit it. -- `.context/architecture/`: system structure and integration patterns. -- `.context/decisions/`: durable tradeoffs and decisions. -- `.context/failures/`: known bad paths and failed approaches. -- `.context/runbooks/`: operational procedures. -- `.context/summaries/`: synthesized project history. +Durable project knowledge belongs in tracked docs: -Do not dump raw transcripts, logs, screenshots, or secrets into `.context/`. +- Architecture and layout: `README.md`, `docs/template-proposal.md`, `docs/pattern-report.md`. +- Tooling decisions: `docs/tooling.md`, `docs/supply-chain.md`. +- Local development runbooks: `docs/development.md`. +- Repeated failure patterns: concise tracked docs, not raw logs or transcripts. ## Validation diff --git a/CLAUDE.md b/CLAUDE.md index 0dea61f..c2eb0b3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -5,7 +5,8 @@ The canonical agent instructions are in `AGENTS.md`. Follow the same repo rules as Codex: - Read before editing. -- Prefer `uv run`, `bun run`, and scripts in `scripts/`. +- Prefer repo-provided scripts. Use `bun run` for root tooling and `uv run` when a Python workspace is present. - Keep changes scoped. - Update `.env.example`, tests, and docs when contracts change. -- Use `.context/` for concise operational memory. +- Use gitignored `.context/` for concise workspace-local operational memory. + Promote durable knowledge into tracked docs instead of committing `.context/`. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index dd82e97..c5d73bb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,12 +1,12 @@ # Contributing -This repository is a reference scaffold. Changes should improve conventions that apply across many 508.dev projects without turning the repo into a product-specific app. +This repository is a reference scaffold. Changes should improve conventions that apply across many projects without turning the repo into a product-specific app. ## Principles - Prefer small, composable defaults over large generated frameworks. - Keep root files broadly useful. -- Put team-specific, platform-specific, or workflow-heavy choices in `alternates/`. +- Put language/runtime conventions in `stacks/` and team-specific, platform-specific, or workflow-heavy choices in `extras/`. - Preserve supply-chain cooldowns and committed lockfiles. - Update agent-facing guidance when conventions change. @@ -31,3 +31,10 @@ Before opening or updating a PR, run: Use the PR template. Include what changed, why it belongs in the devkit, and how it was validated. Avoid committing local state such as `.venv`, `node_modules`, caches, raw logs, screenshots, and `.context/artifacts/`. + +## Agent Notes + +- Keep convention changes paired with docs and skill updates. +- Do not turn stack examples into root defaults without explaining why the + convention applies across most projects. +- Validate both the root template and any stack touched by the change. diff --git a/DECISIONS.md b/DECISIONS.md index 1404b0c..0de1bc8 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -1,6 +1,6 @@ # 508 Devkit Decisions -Last reviewed: 2026-06-02 +Last reviewed: 2026-06-03 This is the constitution for the devkit. Use it as the decision authority; use files in this repo as examples or frozen primitives according to the decision below. @@ -8,7 +8,7 @@ This is the constitution for the devkit. Use it as the decision authority; use f Decision: keep and test files that agents would not reliably reproduce correctly without this repo. Treat ordinary app code as disposable examples. -Why: agents can generate plausible FastAPI handlers, workers, and frontend apps. They are less reliable at reproducing 508.dev-specific topology, safety policy, worktree behavior, and operational memory conventions. +Why: agents can generate plausible FastAPI handlers, workers, and frontend apps. They are less reliable at reproducing devkit-specific topology, safety policy, worktree behavior, and operational memory conventions. Deviate when: a real target repo needs product code. Generate it for that repo instead of copying placeholder app code from the devkit. @@ -28,11 +28,11 @@ Why: agents and humans need reproducible dependency resolution. Deviate when: a repo is intentionally a library template without a runnable dependency graph. Document why no lockfile is committed. -## Bun Default, pnpm Alternate +## Bun First, pnpm First-Class -Decision: use Bun for new JavaScript workspaces; use pnpm when a large workspace needs pnpm-specific monorepo behavior. +Decision: show Bun first for JavaScript workspaces while keeping pnpm first-class for teams, repos, or large workspaces that prefer pnpm-specific monorepo behavior. -Why: Bun is fast and simple for greenfield repos. pnpm remains a good fit for larger established JS workspaces. +Why: Bun is fast and simple for greenfield repos, and the author prefers it. That preference should not imply pnpm is second-class or wrong for new projects. Deviate when: the target repo already uses pnpm, npm, Yarn, or another package manager for a clear reason. Do not churn package managers during unrelated work. @@ -48,7 +48,7 @@ Deviate when: the target repo has already chosen a framework or the user explici Decision: run app processes on the host and infrastructure through Docker Compose during local development. -Why: host app processes are easier for agents to inspect and faster for reload loops. Compose still standardizes Postgres, Redis, and similar infrastructure. +Why: host app processes are easier for agents to inspect and faster for reload loops. Compose still provides concrete examples for local infrastructure such as databases and caches. Deviate when: deployment parity, binary dependencies, or team policy require full-container development. Put that in docs and scripts explicitly. @@ -60,17 +60,17 @@ Why: sibling worktrees should run concurrently without hand-editing `.env` files Deviate when: a platform assigns ports dynamically. Preserve the script for local development unless it is truly irrelevant. -## `.context/` Operational Memory +## `.context/` Workspace Memory -Decision: keep `.context/` as operational memory for humans and agents. +Decision: keep `.context/` gitignored as workspace-local operational memory for humans and agents. Do not ship it as tracked template content. Why: architecture notes, decisions, failures, runbooks, and summaries prevent repeated failed approaches and preserve local reasoning. -Deviate when: information is user-facing or contributor-facing. Put that in README, docs, or official project documentation instead. +Deviate when: information is durable, user-facing, or contributor-facing. Put that in README, docs, or official project documentation instead. ## GitHub Hygiene -Decision: include small issue/PR templates, least-privilege workflows, pinned action SHAs, secret scanning, dependency review, and Renovate cooldown policy. +Decision: include small issue/PR templates, least-privilege workflows, pinned action SHAs, and Renovate cooldown policy. Keep Gitleaks and Dependency Review as opt-in extras: Gitleaks can create noisy baseline findings, and Dependency Review depends on GitHub's dependency graph and is primarily vulnerability/license/change reporting rather than active supply-chain attack detection. Why: collaboration and security hygiene are broadly useful and easy to drift across repos. diff --git a/README.md b/README.md index e31d924..326d45d 100644 --- a/README.md +++ b/README.md @@ -1,31 +1,32 @@ # 508 Devkit -Last reviewed: 2026-06-02 +Last reviewed: 2026-06-03 -Opinionated sane defaults and conventions for 508.dev projects. +Opinionated sane defaults and conventions for software projects. -This is not a scaffolding CLI. It is a reference repo that gives agents and humans a shared baseline for how new projects should be shaped: repository layout, local development, dependency safety, CI, agent instructions, operational memory, and documentation. +This is not a scaffolding CLI. It is a reference repo that gives agents and humans a shared baseline for how new projects should be shaped: repository layout, local development, dependency safety, CI, agent instructions, operational memory, and documentation. It comes from 508.dev practice, but it is meant to be useful outside 508.dev too. Point an agent at this repo when starting or normalizing a project. The agent should inspect the target repo, ask clarifying questions when the product or stack is ambiguous, and then copy or adapt only the conventions that fit. ## What It Captures - Agent-native instructions for Codex, Claude Code, Cursor, and future agents. -- `.context/` operational memory conventions. -- Bun as the default JavaScript package manager for new projects. -- `uv` for Python. -- Dependency cooldowns for Bun, pnpm fallback, and uv. +- Gitignored workspace-local `.context/` conventions for Conductor and agents. +- Bun and pnpm as first-class JavaScript package manager examples, with Bun + shown first. +- Optional `uv` Python workspace conventions. +- Dependency cooldowns for Bun, pnpm, and uv. - Host-run development services. -- Docker Compose for Postgres, Redis, and optional MinIO. +- Docker Compose examples for local infrastructure such as databases and + caches. - Deterministic worktree ports. - Framework-neutral frontend conventions. - `.worktreeinclude` for copying local-only env files into sibling worktrees. - `.dockerignore` for small, secret-safe Docker build contexts. - GitHub PR, issue, and CI hygiene. - Security scanning and dependency update policy. -- Pydantic settings and schemas. -- Alembic migrations for Python services. -- Drizzle ORM for TypeScript services. +- Pydantic settings/schema and Alembic migration examples for Python services. +- Drizzle ORM examples for TypeScript services. - Ruff, MyPy, Pytest, Biome, and Vitest. - Optional SOPS documentation without forcing SOPS into every repo. @@ -34,14 +35,16 @@ Point an agent at this repo when starting or normalizing a project. The agent sh Use the repo directly: ```text -Use /path/to/508-devkit as the project bootstrap reference. +Use https://github.com/508-dev/508-devkit or a local checkout of it as the +project bootstrap reference. Inspect my target repo, ask any necessary questions, then apply the relevant conventions. ``` -Or install/use the bundled agent skill: +Or install/use the bundled agent skill from this repo: ```text -Install the skill from skills/508-devkit/SKILL.md. +Install the skill from https://raw.githubusercontent.com/508-dev/508-devkit/main/skills/508-devkit/SKILL.md +or from skills/508-devkit/SKILL.md in a local checkout. Run it as /508-devkit, /bootstrap-project, or whatever command name your agent client assigns. ``` @@ -50,13 +53,12 @@ Expected agent behavior: - Inspect the target repo before editing. - Ask about product shape, deployment target, data stores, and language/runtime choices when those are unclear. - Automatically pick up existing conventions when the repo already has them. -- Prefer the devkit defaults for new projects unless there is a clear reason to choose an alternate. +- Prefer the devkit defaults for new projects unless there is a clear reason to choose a stack or extra. - Run the narrowest relevant checks before calling the bootstrap complete. For this repo itself: ```bash -uv sync bun install --frozen-lockfile ./scripts/check-all.sh ./scripts/dev.sh @@ -65,24 +67,23 @@ bun install --frozen-lockfile ## Layout ```text -apps/api Minimal Python wiring example, Pydantic settings, SQLAlchemy/Alembic -apps/web Framework-neutral TypeScript wiring example, Drizzle, Biome, Vitest -packages/shared Shared Python contracts and helpers +stacks Language/runtime conventions such as TypeScript and Python scripts Stable human/agent entrypoints docs Durable project documentation -.context Operational memory +extras Optional workflow, deployment, and support add-ons ``` ## Read Next 1. Read `DECISIONS.md`. 2. Read `docs/pattern-report.md`. -3. Read `docs/template-proposal.md`. -4. Read `docs/frontend.md`. -5. Copy `.env.example` to `.env`. -6. Run `./scripts/worktree-ports.py env`. -7. Run `./scripts/docker-compose.sh up -d postgres redis`. -8. Run `./scripts/dev.sh`. +3. Read `docs/tooling.md`. +4. Read `docs/template-proposal.md`. +5. Read `docs/frontend.md`. +6. Copy `.env.example` to `.env`. +7. Run `./scripts/worktree-ports.sh env`. +8. Run `./scripts/docker-compose.sh up -d postgres redis`. +9. Run `./scripts/dev.sh`. ## Worktree And Docker Hygiene @@ -90,25 +91,54 @@ Keep `.worktreeinclude` as a short allowlist of ignored local files that should Keep `.dockerignore` broad enough to exclude VCS metadata, agent scratch state, dependencies, caches, logs, and secrets from Docker build contexts. Make exceptions only for committed templates such as `.env.example`. +Do not commit `.context/`. Conductor creates `.context/` as workspace-local +agent scratch. Durable project knowledge belongs in normal docs such as +`docs/tooling.md`, `docs/development.md`, `docs/pattern-report.md`, or +`README.md`. + ## Package Manager Policy -Bun is the default for new JavaScript projects. +Bun is shown first because it is the author's preference and keeps small +projects simple, but it is not a universal requirement. -pnpm remains documented as a fallback for larger JS workspaces. If switching to pnpm, add `pnpm-workspace.yaml`, set `minimumReleaseAge: 10080`, and change CI install commands to `pnpm install --frozen-lockfile`. +pnpm is first-class for teams or workspaces that already prefer it, need its +monorepo behavior, or want the broader pnpm ecosystem. If using pnpm, add +`pnpm-workspace.yaml`, set `minimumReleaseAge: 10080`, and change CI install +commands to `pnpm install --frozen-lockfile`. -## Pick-And-Choose Alternates +## Pick-And-Choose Stacks And Extras This repository intentionally includes files that conflict with each other. It is a starter template, not an installable preset. -- `alternates/pnpm/`: pnpm root files and CI fragment. -- `alternates/dev-scripts/`: JS-first script variants for repos that do not want Python helpers. -- `alternates/dockerfiles/`: opt-in Dockerfile examples for deployment parity. -- `alternates/devcontainer/`: opt-in dev container example. -- `alternates/github/`: CODEOWNERS and discussion templates that need project-specific owners or support policy. -- `alternates/todo-to-issue/`: opt-in workflow for turning TODO comments into GitHub issues. +- `stacks/typescript/`: framework-neutral TypeScript conventions, Drizzle + examples, Biome, Vitest. +- `stacks/python/`: optional Python API/shared-package workspace. +- `stacks/typescript/pnpm/`: pnpm root files and CI fragment for larger TypeScript workspaces. +- `extras/dev-scripts/`: JS-first script variants and JS implementations of helper scripts. +- `extras/dockerfiles/`: opt-in Dockerfile examples for deployment parity. +- `extras/devcontainer/`: opt-in dev container example. +- `extras/object-storage/`: very opt-in MinIO Compose example for projects + that need local S3-compatible storage. +- `extras/github/`: CODEOWNERS and discussion templates that need project-specific owners or support policy. +- `extras/todo-to-issue/`: opt-in workflow for turning TODO comments into GitHub issues. - `.sops.yaml.example`: optional SOPS starter only for repos that need encrypted files. -Keep root defaults for most new projects: Bun, `uv`, shell wrappers, Python worktree ports, and Compose-managed infra. Treat `apps/*` as wiring examples to regenerate or adapt, not product code to copy blindly. +Keep root defaults for most new projects: shell wrappers, shell worktree ports, and example Compose-managed infra. Select language/runtime stacks such as `stacks/typescript/`, `stacks/python/`, future `stacks/go/`, or future `stacks/rust/` based on the target project. Treat stack files as conventions to adapt, not product code to copy blindly. + +## Agent Notes + +- Never copy the whole repository into a target project. +- Start with root hygiene files, then select only the stacks and extras that + match the target repo. +- Treat `stacks/` as peer language/runtime convention packs. TypeScript and + Python are examples, not universal defaults. +- Treat `extras/` as opt-in workflows or deployment helpers that may require + repo settings, real owners, or team process. +- Keep `.context/` gitignored. Promote durable learnings into tracked docs + instead of committing agent scratch. +- Keep worktree port helpers generic. If a workspace orchestrator exposes a + reserved port or port block, map it to `WORKTREE_PRIMARY_PORT` or + `WORKTREE_PORT_BLOCK_START` outside the helper. ## Skill Interface diff --git a/SECURITY.md b/SECURITY.md index 437e320..4602971 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -18,10 +18,23 @@ Report security concerns to the project maintainers through the private channel This devkit uses dependency cooldowns and locked installs: - Bun: `minimumReleaseAge = 604800`. -- uv: `exclude-newer = "7 days"`. +- uv: `exclude-newer = "P7D"` when a Python workspace is present and uv is + `0.9.17` or newer. - Renovate: `minimumReleaseAge = "7 days"`. - CI should use frozen or locked installs. ## GitHub Actions Workflows should use least-privilege permissions, pinned action SHAs, `persist-credentials: false` where practical, and `harden-runner` in audit or block mode. + +## Agent Notes + +- Do not leave the placeholder reporting channel in a real repository. +- Prefer GitHub native secret scanning and push protection where available. +- Add `extras/github/gitleaks.yml.example` only when maintainers want CI secret + scanning and can triage findings. +- Add `extras/github/dependency-review.yml.example` only when dependency graph + reporting is desired and enabled. +- Check `uv --no-config --version` before adding relative uv cooldowns to a + downstream repo. Ask before upgrading uv; do not leave old uv clients with + unparseable `P7D` or `7 days` config. diff --git a/alternates/dev-scripts/README.md b/alternates/dev-scripts/README.md deleted file mode 100644 index 1d3b64c..0000000 --- a/alternates/dev-scripts/README.md +++ /dev/null @@ -1,17 +0,0 @@ -# Dev Script Alternates - -The root template keeps shell wrappers as the canonical entrypoints: - -- `scripts/dev.sh` -- `scripts/docker-compose.sh` -- `scripts/check-all.sh` - -That is intentional. Shell wrappers are easy for humans, CI, and agents to discover, and they can delegate to Python or Bun where those tools are better. - -Recommended split: - -- Use `.sh` for stable top-level commands and process orchestration. -- Use Python for dependency-free deterministic logic that must work before JS dependencies are installed, such as worktree ports. -- Use `.mjs` or `.ts` for JS-only projects where the script directly interacts with Vite, Next.js, Drizzle, or TypeScript config. - -This directory provides examples for JS-first repos that want to replace the Python port helper. diff --git a/alternates/devcontainer/README.md b/alternates/devcontainer/README.md deleted file mode 100644 index 7945abf..0000000 --- a/alternates/devcontainer/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# Dev Container Alternate - -Use this alternate only when the team wants a containerized editor/runtime environment. - -It is not a root default because many 508.dev repos prefer host-run app services for reload speed and agent debuggability. - -Copy `devcontainer.json.example` to `.devcontainer/devcontainer.json` and adapt ports, extensions, and post-create commands for the target repo. diff --git a/alternates/github/README.md b/alternates/github/README.md deleted file mode 100644 index c9456aa..0000000 --- a/alternates/github/README.md +++ /dev/null @@ -1,13 +0,0 @@ -# GitHub Alternates - -These files are useful in many repositories but should be copied intentionally. - -## CODEOWNERS - -`CODEOWNERS.example` is a starting point for review ownership. Do not commit it as active `.github/CODEOWNERS` until the owners are real GitHub users or teams. - -Active CODEOWNERS affects reviewer routing and may interact with branch protection, so keep ownership broad at first and tighten it as the team stabilizes. - -## Community Templates - -Use `community/DISCUSSION_TEMPLATE/questions.yml` only for repositories that use GitHub Discussions for public or internal support. Keep the language project-specific and avoid making the template longer than the support workflow can justify. diff --git a/alternates/pnpm/README.md b/alternates/pnpm/README.md deleted file mode 100644 index 9730c9d..0000000 --- a/alternates/pnpm/README.md +++ /dev/null @@ -1,22 +0,0 @@ -# pnpm Alternate - -The root template is Bun-first. Use these files when a project needs pnpm instead, usually for a larger JavaScript workspace or a team that already standardizes on pnpm. - -Copy these files over the root equivalents: - -- `package.json` -- `pnpm-workspace.yaml` - -Then update CI install commands from: - -```bash -bun install --frozen-lockfile -``` - -to: - -```bash -pnpm install --frozen-lockfile -``` - -Keep `bunfig.toml` out of pnpm projects unless Bun is still used for local scripts. diff --git a/alternates/pnpm/package.json b/alternates/pnpm/package.json deleted file mode 100644 index 79bd59a..0000000 --- a/alternates/pnpm/package.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "name": "508-devkit", - "version": "0.1.0", - "private": true, - "type": "module", - "packageManager": "pnpm@10.33.0", - "scripts": { - "dev": "./scripts/dev.sh", - "build": "pnpm -C apps/web run build", - "lint": "biome lint .", - "format": "biome format --write .", - "format:check": "biome format .", - "typecheck": "pnpm -C apps/web run typecheck", - "test": "pnpm -r run test", - "check": "./scripts/check-all.sh", - "ports": "python3 scripts/worktree-ports.py env", - "db:generate": "pnpm -C apps/web run db:generate", - "db:push": "pnpm -C apps/web run db:push", - "db:studio": "pnpm -C apps/web run db:studio" - }, - "devDependencies": { - "@biomejs/biome": "^2.4.13", - "@types/node": "^22.15.29", - "typescript": "^5.9.3" - } -} diff --git a/apps/web/src/index.ts b/apps/web/src/index.ts deleted file mode 100644 index 914fc85..0000000 --- a/apps/web/src/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export function apiBaseUrl(env: Record = {}): string { - return env.WEB_API_BASE_URL ?? "http://127.0.0.1:8720"; -} diff --git a/biome.json b/biome.json index b22d5a1..7c270f1 100644 --- a/biome.json +++ b/biome.json @@ -3,7 +3,7 @@ "root": true, "files": { "ignoreUnknown": true, - "includes": ["**", "!**/.venv", "!**/.mypy_cache", "!**/.pytest_cache"] + "includes": ["**", "!**/.venv", "!**/.claude", "!**/.mypy_cache", "!**/.pytest_cache"] }, "formatter": { "enabled": true, diff --git a/bun.lock b/bun.lock index 62b6f3a..9104999 100644 --- a/bun.lock +++ b/bun.lock @@ -10,7 +10,7 @@ "typescript": "^5.9.3", }, }, - "apps/web": { + "stacks/typescript": { "name": "@508-devkit/web", "version": "0.1.0", "dependencies": { @@ -26,7 +26,7 @@ }, }, "packages": { - "@508-devkit/web": ["@508-devkit/web@workspace:apps/web"], + "@508-devkit/web": ["@508-devkit/web@workspace:stacks/typescript"], "@biomejs/biome": ["@biomejs/biome@2.4.15", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.4.15", "@biomejs/cli-darwin-x64": "2.4.15", "@biomejs/cli-linux-arm64": "2.4.15", "@biomejs/cli-linux-arm64-musl": "2.4.15", "@biomejs/cli-linux-x64": "2.4.15", "@biomejs/cli-linux-x64-musl": "2.4.15", "@biomejs/cli-win32-arm64": "2.4.15", "@biomejs/cli-win32-x64": "2.4.15" }, "bin": { "biome": "bin/biome" } }, "sha512-j5VH3a/h/HXTKBM50MDMxRCzkeLv9S2XJcW2WgnZT1+xyisi+0bISrXR82gCX+8S9lvK0skEvHJRN+3Ktr2hlw=="], diff --git a/compose.yml b/compose.yml index 6dd8bf2..822a4c6 100644 --- a/compose.yml +++ b/compose.yml @@ -6,6 +6,9 @@ services: POSTGRES_USER: ${POSTGRES_USER:-app} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-app} ports: + # Bind to localhost by default. scripts/docker-compose.sh injects + # worktree-specific host ports so parallel Conductor workspaces do not + # fight over one fixed Postgres port. - "${POSTGRES_HOST_BIND:-127.0.0.1}:${POSTGRES_HOST_PORT:-8740}:5432" volumes: - postgres-data:/var/lib/postgresql/data @@ -19,6 +22,8 @@ services: image: redis:7-alpine command: ["redis-server", "--save", "60", "1", "--loglevel", "warning"] ports: + # Keep Redis host binding configurable for rare LAN/container cases, but + # default to localhost for development safety. - "${REDIS_HOST_BIND:-127.0.0.1}:${REDIS_HOST_PORT:-8750}:6379" volumes: - redis-data:/data @@ -28,25 +33,6 @@ services: timeout: 3s retries: 10 - minio: - image: cgr.dev/chainguard/minio - command: ["server", "/data", "--console-address", ":9001"] - profiles: ["object-storage"] - environment: - MINIO_ROOT_USER: ${MINIO_ROOT_USER:-internal} - MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:-change-me} - ports: - - "${MINIO_HOST_BIND:-127.0.0.1}:${MINIO_API_HOST_PORT:-8760}:9000" - - "${MINIO_HOST_BIND:-127.0.0.1}:${MINIO_CONSOLE_HOST_PORT:-8761}:9001" - volumes: - - minio-data:/data - healthcheck: - test: ["CMD-SHELL", "mc ready local || exit 1"] - interval: 10s - timeout: 3s - retries: 10 - volumes: postgres-data: redis-data: - minio-data: diff --git a/docs/agent-walkthrough.md b/docs/agent-walkthrough.md index fefe05e..ea1a59f 100644 --- a/docs/agent-walkthrough.md +++ b/docs/agent-walkthrough.md @@ -30,12 +30,20 @@ Inspect my target repo, ask any necessary questions, then apply the relevant con ## Example Decisions -If the target repo has no frontend framework, copy the framework-neutral `apps/web` conventions but do not scaffold Next.js, Vite, or TanStack Start. +If the target repo has no frontend framework, copy the framework-neutral `stacks/typescript` conventions but do not scaffold Next.js, Vite, or TanStack Start. -If the target repo already uses pnpm, use `alternates/pnpm/` instead of forcing Bun. +If the target repo already uses pnpm, use `stacks/typescript/pnpm/` instead of forcing Bun. If the target repo has a deployment platform, update `docs/deployment.md`. If not, leave a decision record placeholder. -If the target repo is public or support-heavy, consider `alternates/github/community/`. Otherwise keep discussion templates out. +If the target repo is public or support-heavy, consider `extras/github/community/`. Otherwise keep discussion templates out. If the target repo has no real GitHub teams yet, do not enable active CODEOWNERS. + +If maintainers ask for secret scanning, prefer GitHub native secret scanning +and push protection when available. Add the Gitleaks extra only when CI scanning +is explicitly desired. + +If maintainers ask for dependency visibility, explain that Dependency Review is +dependency graph-based reporting. Add the extra only after confirming the graph +is enabled and the repo wants that reporting. diff --git a/docs/deployment.md b/docs/deployment.md index a788d51..d212535 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -36,3 +36,12 @@ Before enabling automatic production deploys: 3. Confirm rollback behavior. 4. Keep preview deploys separate from production deploys. 5. Use least-privilege deployment credentials. + +## Agent Notes + +- Do not infer a deployment platform from this devkit. Inspect the target repo, + hosting account, and team preference first. +- Keep deployment workflows out of new repos until secrets and rollback are + known. +- If deployment is undecided, leave the decision record blank rather than + copying placeholder platform files. diff --git a/docs/development.md b/docs/development.md index 0ee7dc5..a7a7c73 100644 --- a/docs/development.md +++ b/docs/development.md @@ -9,14 +9,33 @@ Local development follows the pattern used across existing projects: ## Commands ```bash -./scripts/worktree-ports.py env +./scripts/worktree-ports.sh env ./scripts/docker-compose.sh up -d postgres redis ./scripts/dev.sh +./scripts/check-all.sh ``` +## Worktree Port Reservations + +`scripts/worktree-ports.sh` normally hashes the absolute git worktree path and +derives stable local ports from that hash. Some agent/worktree orchestrators +reserve ports for each workspace. Keep product-specific environment variable +names out of the helper and map them to these generic names in the run command +or wrapper script: + +- `WORKTREE_PORT_BLOCK_START`: first port in a reserved block. +- `WORKTREE_PORT_BLOCK_SIZE`: size of the reserved block, default `10`. +- `WORKTREE_PRIMARY_PORT`: one reserved public port. +- `WORKTREE_PRIMARY_PORT_TARGET`: `WEB_PORT` or `API_PORT`, default `WEB_PORT`. + +When a block is present, the helper uses compact offsets inside it for web, API, +worker health, database, cache, and OTEL example ports. When only one public port +is present, the helper assigns it to the selected primary target and keeps other +ports on the normal deterministic worktree allocation. + ## Worktree Includes -Use `.worktreeinclude` to allowlist ignored local files that should be copied into new sibling worktrees. +Use `.worktreeinclude` to allowlist ignored local files that should be copied into new sibling worktrees. Treat entries as gitignore-style path patterns, not shell globs passed directly to `cp`. Example: @@ -29,6 +48,12 @@ Example: Do not include generated state such as `.venv`, `node_modules`, caches, local databases, screenshots, or raw logs. Those should be recreated per worktree. +## Workspace Context + +Do not commit `.context/`. Conductor creates it as workspace-local scratch for +agents. Durable runbooks and decisions belong in tracked docs such as this file, +`docs/tooling.md`, and `docs/pattern-report.md`. + ## Docker Build Contexts Keep `.dockerignore` in every repo that has Dockerfiles or Compose services. Exclude local secrets, dependency directories, caches, agent scratch state, and build outputs so Docker does not upload large or sensitive files into the build context. @@ -57,3 +82,11 @@ build Host-run app services are faster for reload loops, easier for agents to inspect, and avoid rebuilding containers for normal code changes. Use full-container Compose only when validating deployment parity. + +## Agent Notes + +- Keep root scripts as stable entrypoints. Change package-manager internals + behind them when adapting a target repo. +- Use `./scripts/worktree-ports.sh env` before debugging port conflicts. +- Copy ignored local config through `.worktreeinclude`; do not commit copied + `.env` files or generated workspace state. diff --git a/docs/frontend.md b/docs/frontend.md index 0ff70d9..4e9f30e 100644 --- a/docs/frontend.md +++ b/docs/frontend.md @@ -6,13 +6,14 @@ Next.js, Vite, TanStack Start, Astro, Expo, and other frameworks are all reasona ## Root Convention -The root `apps/web` package is a framework-neutral TypeScript workspace. It exists to capture shared JavaScript conventions: +The `stacks/typescript` package is a framework-neutral TypeScript workspace. It exists to capture shared JavaScript conventions: - Bun package scripts. - TypeScript typechecking. - Biome formatting and linting. - Vitest unit tests. -- Drizzle placeholder config for TypeScript-side database access when needed. +- Drizzle placeholder config as one TypeScript-side database access example + when needed. It is not meant to be copied as a finished web application. @@ -39,3 +40,7 @@ When bootstrapping a target repo, agents should ask or infer: - Which frontend conventions already exist in the target repo? If the answer is unclear, leave the frontend framework unselected and document the decision needed. + +When adding a frontend later, update the public environment variable mapping, +CI job names, and development docs together so agents do not mix framework +conventions. diff --git a/docs/github-workflows.md b/docs/github-workflows.md index ee93336..14985e2 100644 --- a/docs/github-workflows.md +++ b/docs/github-workflows.md @@ -11,27 +11,61 @@ Root `.github/` files are meant to be safe defaults for most repositories: - `.github/ISSUE_TEMPLATE/feature_request.yml`: captures product or workflow requests. - `.github/ISSUE_TEMPLATE/docs_request.yml`: captures documentation gaps. - `.github/ISSUE_TEMPLATE/config.yml`: keeps blank issues allowed and documents where to add discussion links. -- `.github/workflows/ci.yml`: runs the baseline Python, web, and Compose checks. -- `.github/workflows/security.yml`: runs secret scanning and dependency review. +- `.github/workflows/ci.yml`: runs baseline web/tooling, Python stack, and Compose checks. Keep these templates short. They should improve issue and PR quality without making lightweight collaboration feel bureaucratic. Workflows pin third-party actions to commit SHAs and use `harden-runner` in audit mode. When applying this devkit, update pinned SHAs intentionally rather than floating back to moving tags. +Plain checkout-and-run validation jobs should usually keep `contents: read` only. +Do not add `pull-requests: read` unless the job explicitly calls PR APIs or +posts PR comments. + +If path-filtered jobs are used with branch protection, require a final aggregate +job such as `ci-passed` instead of requiring every skipped job directly. +When using `dorny/paths-filter` without `pull-requests: read`, set `token: ""` +and check out enough history so the action uses git-based detection. + +## Security Extras + +Secret scanning and Dependency Review are extras, not default workflows. + +Use `extras/github/gitleaks.yml.example` only when maintainers want CI history +scanning for secrets and are ready to triage baseline findings or false +positives. + +Use `extras/github/dependency-review.yml.example` only when maintainers want +GitHub dependency graph-based vulnerability, license, or dependency-change +reporting. + +## Dependency Review + +Dependency Review is not part of the default security workflow. It depends on +GitHub's dependency graph and is best treated as vulnerability, license, and +dependency-change reporting, not as the primary active supply-chain attack +detector. + +Use `extras/github/dependency-review.yml.example` only when the repo owner wants +GitHub dependency graph-based reporting. To enable it in a target repo: + +1. Enable GitHub's dependency graph for the repository. +2. Copy `extras/github/dependency-review.yml.example` to `.github/workflows/dependency-review.yml`. +3. Keep the workflow opt-in; do not gate it on `repository.private == false`. + ## CODEOWNERS -Do not enable CODEOWNERS with placeholders. Copy `alternates/github/CODEOWNERS.example` to `.github/CODEOWNERS` only after replacing owners with real GitHub users or teams. +Do not enable CODEOWNERS with placeholders. Copy `extras/github/CODEOWNERS.example` to `.github/CODEOWNERS` only after replacing owners with real GitHub users or teams. Start broad, then make ownership more specific as code ownership becomes real. CODEOWNERS can affect required reviews and branch protection, so stale entries create workflow friction. ## Discussions -Use `alternates/github/community/DISCUSSION_TEMPLATE/questions.yml` only when the repository uses GitHub Discussions for support or product feedback. +Use `extras/github/community/DISCUSSION_TEMPLATE/questions.yml` only when the repository uses GitHub Discussions for support or product feedback. Discussion templates should be lighter than issue templates. Ask for the question, context, and a minimal example; avoid long pledges or community rules unless the project has an explicit support policy. ## TODO To Issue Automation -Use `alternates/todo-to-issue/` only as an opt-in workflow. It can be useful for codebase maintenance, but it grants write permissions and can create issue noise. +Use `extras/todo-to-issue/` only as an opt-in workflow. It can be useful for codebase maintenance, but it grants write permissions and can create issue noise. Prefer manual runs first. Promote to scheduled or merge-triggered runs only after the team agrees on identifiers, labels, ownership, and whether source files should receive issue links. diff --git a/docs/interfaces.md b/docs/interfaces.md index 0a60ae9..af99fc5 100644 --- a/docs/interfaces.md +++ b/docs/interfaces.md @@ -31,3 +31,11 @@ Keep both: - `508-devkit` skill: lightweight instructions that point at this repo and describe how to choose files. The skill should not duplicate every template file. It should reference this repository and teach the agent how to apply it. + +## Agent Notes + +- Update the repository files first; the skill should summarize how to apply + them, not fork their content. +- When a convention changes, update both the tracked docs and + `skills/508-devkit/SKILL.md` so agents do not apply stale behavior. +- Keep examples short enough that humans can review the policy in a PR. diff --git a/docs/observability.md b/docs/observability.md index fcb49c2..9f09918 100644 --- a/docs/observability.md +++ b/docs/observability.md @@ -18,3 +18,11 @@ Add Sentry when the project has a real error reporting destination and release/e Add OpenTelemetry when the project has a collector, trace sampling policy, and service naming convention. Keep SDK initialization in one shared helper per runtime so API, worker, and job processes behave consistently. + +## Agent Notes + +- Do not add telemetry SDK dependencies just because the env vars exist. +- Add Sentry, OTEL, metrics, or log shipping only after the target repo has a + destination and naming convention. +- Keep one runtime-local initialization helper so services do not configure + observability differently by accident. diff --git a/docs/pattern-report.md b/docs/pattern-report.md index 0617b52..b241b61 100644 --- a/docs/pattern-report.md +++ b/docs/pattern-report.md @@ -10,7 +10,7 @@ The scan found 32 Git repositories. The strongest repeated signals were: | --- | ---: | --- | | Python or `uv` project metadata | 13 | `pyproject.toml` plus `uv.lock` appears across backend, worker, pipeline, and agent/tooling repos. | | Node package metadata | 17 | Large monorepos lean `pnpm`; smaller single-app repos frequently use Bun. | -| Docker Compose | 10 | Most Compose files provide infrastructure services first, especially Postgres and Redis. | +| Docker Compose | 10 | Most Compose files provide infrastructure services first, often databases or caches such as Postgres and Redis. | | GitHub Actions workflows | 20 | Common CI shape is lint, typecheck, test, and deploy workflows split by changed area. | | Pre-commit config | 6 | Most Python-heavy repos use Ruff hooks; some also run MyPy through local scripts. | | Agent instruction files | 14 | `AGENTS.md` is common for Codex. `CLAUDE.md` is present in several repos. | @@ -39,9 +39,7 @@ Representative repositories inspected more deeply: - Pytest for backend and worker tests. - Coverage for mature service repos. - `508-workflows` uses a `uv` workspace with `apps/*` and `packages/shared`. -- Dependency cooldown appears in `508-workflows` through: - - `[tool.uv] exclude-newer = "7 days"` - - `[tool.uv.pip] exclude-newer = "7 days"` +- Dependency cooldown appears in `508-workflows`; use `exclude-newer = "P7D"` only after checking `uv --no-config --version` is `0.9.17` or newer, and regenerate `uv.lock` so it records `exclude-newer-span = "P7D"`. ### JavaScript and TypeScript @@ -59,7 +57,7 @@ Representative repositories inspected more deeply: - `test` - `ports` -Template decision after user preference: default to Bun for new projects. Document pnpm as the fallback for very large workspaces that need its mature monorepo ergonomics. +Template decision after user preference: show Bun first for new JavaScript examples while keeping pnpm first-class for teams or workspaces that prefer its mature monorepo ergonomics. ### Docker Compose and Infra @@ -69,18 +67,22 @@ Common services: - `postgres` - `redis` -- Optional object storage, especially MinIO in `508-workflows`. +- Optional object storage in projects that explicitly need it. - Optional observability stacks in larger repos, such as HyperDX or OTEL endpoints. Common Compose practices: -- Healthchecks on Postgres and Redis. +- Healthchecks on local infrastructure services. - Host ports controlled by environment variables. - Fixed container ports with variable published host ports. - Volumes for durable local data. - `compose.yml` or `compose.yaml` as the canonical file, with occasional `docker-compose.yml` compatibility wrappers. -Template decision: include Postgres and Redis by default, plus optional MinIO behind a Compose profile. +Template decision: include Postgres and Redis as concrete Compose examples, but +make clear they are replaceable and not a universal requirement. Keep object +storage out of the root template unless a target repo explicitly needs it. Keep +the MinIO pattern as a very opt-in `extras/object-storage/` example because +server/client image behavior has been a source of downstream friction. ### Local Development @@ -94,13 +96,15 @@ Examples: - `asiatraveldeals` uses `scripts/run-local.sh` to start Compose infra and host-run API/web services. - `favorite-places`, `house-calendar`, `voy`, and `asiatraveldeals` include deterministic worktree port allocation. -Template decision: include dependency-free `scripts/worktree-ports.py`, `scripts/dev.sh`, and `scripts/docker-compose.sh`. +Template decision: include dependency-free `scripts/worktree-ports.sh`, `scripts/dev.sh`, and `scripts/docker-compose.sh`. Keep the Python port helper in `stacks/python/` for Python-first repos. ### Worktree Support and Ports Recurring approach: - Hash the absolute worktree path. +- Respect generic reserved port inputs when a worktree orchestrator provides + them. - Reserve the main checkout or default block where applicable. - Derive stable port offsets for sibling worktrees. - Avoid browser-restricted ports. @@ -118,7 +122,10 @@ Common variable names: - `POSTGRES_URL` - `REDIS_URL` -Template decision: allocate a block of ports per worktree and emit API, web, worker health, Postgres, Redis, MinIO, and OTEL ports. +Template decision: allocate a block of ports per worktree and emit API, web, +worker health, database, cache, and OTEL example ports. Keep orchestrator-specific +port environment variables out of reusable helpers; map them to +`WORKTREE_PORT_BLOCK_START` or `WORKTREE_PRIMARY_PORT` in wrapper scripts. ### GitHub Actions @@ -134,7 +141,7 @@ Common workflow traits: - Include deployment workflows separately from validation workflows. - Use concurrency groups for larger deploy/preview workflows. -Template decision: include one CI workflow with changed-area detection and jobs for Python, web, and Compose smoke checks. +Template decision: include one CI workflow with changed-area detection and jobs for root web/tooling checks, Python stack checks, and Compose smoke checks. ### Environment Variables @@ -208,7 +215,7 @@ Template decision: include unit test placeholders, marker conventions, and docs The template should not be a generator. It should be a compact source of truth that agents can read, adapt, and extend: - Agent docs at the root: `AGENTS.md`, `CLAUDE.md`, Cursor rules. -- Operational memory in `.context/`. +- Gitignored workspace-local `.context/` for agent scratch, with durable knowledge promoted into tracked docs. - Clear repo layout and boundaries. - Minimal but real manifests and scripts. - Locked-install and dependency-cooldown defaults. diff --git a/docs/secrets.md b/docs/secrets.md index 1f6fba6..5d9f71e 100644 --- a/docs/secrets.md +++ b/docs/secrets.md @@ -4,6 +4,10 @@ Most repos should use environment variables and CI secrets. SOPS is optional. Add it only when the repository needs encrypted files checked into Git, such as shared non-production config or deploy manifests. +GitHub secret scanning and push protection should be the first choice when they +are available for the repository. Use the Gitleaks extra only when maintainers +also want repo-local CI scanning and are ready to triage historical findings. + If SOPS is adopted: 1. Copy `.sops.yaml.example` to `.sops.yaml`. @@ -12,3 +16,13 @@ If SOPS is adopted: 4. Document decrypt/edit commands in this file. Do not commit plaintext secrets. + +## Agent Notes + +- Replace generic reporting language with the target repo's real private + vulnerability channel before shipping public docs. +- Do not add `.sops.yaml` unless encrypted tracked files are actually needed. +- Do not add the Gitleaks workflow silently. Secret scanning can be noisy until + a baseline and ignore process exist. +- When a real secret is found, tell maintainers to rotate it. Removing it from + git history is not enough. diff --git a/docs/supply-chain.md b/docs/supply-chain.md index 7bd28cf..901ac83 100644 --- a/docs/supply-chain.md +++ b/docs/supply-chain.md @@ -2,7 +2,8 @@ ## Bun -Default for new JavaScript projects. +Shown first for JavaScript projects because it is fast and simple. This is an +example preference, not a universal requirement. `bunfig.toml`: @@ -17,19 +18,49 @@ linker = "isolated" ## uv -`pyproject.toml`: +Use this when a project selects the Python stack or otherwise has a Python workspace. + +Relative dependency cooldowns require uv `0.9.17` or newer. Before adding +`exclude-newer = "P7D"` to a downstream repo, agents should run: + +```bash +uv --no-config --version +``` + +Use `--no-config` for the version check because a broken user-level +`~/.config/uv/uv.toml` can otherwise fail before the project is inspected. + +If uv is older than `0.9.17`, ask the user whether they want to upgrade uv. Do +not write relative values such as `P7D` or `7 days` into `pyproject.toml`, +`uv.toml`, or `~/.config/uv/uv.toml` until the installed uv supports them. Older +uv releases parse those strings as dates and fail during settings discovery. + +Optional `pyproject.toml` cooldown for compatible uv clients: ```toml [tool.uv] -exclude-newer = "7 days" +exclude-newer = "P7D" [tool.uv.pip] -exclude-newer = "7 days" +exclude-newer = "P7D" ``` -## pnpm Fallback +The Python stack does not commit this relative cooldown by default because +older uv clients parse persistent config during settings discovery and fail +before they can run a locked install. Regenerate `uv.lock` with a compatible +`uv` version after adding the setting so the lock records +`exclude-newer-span = "P7D"`. Older `uv` releases and Docker images must be +upgraded before using this relative cooldown. + +If the user declines an upgrade, leave the relative cooldown out of persistent +uv config and document the exception. An absolute RFC 3339 timestamp is +compatible with older uv, but it is a fixed cutoff, not a rolling seven-day +cooldown. + +## pnpm -Use only when the JS workspace grows large enough to justify switching away from Bun. +Use when the team prefers pnpm, the repo already uses pnpm, or the workspace +needs pnpm-specific monorepo behavior. `pnpm-workspace.yaml`: @@ -54,4 +85,13 @@ pnpm install --frozen-lockfile Renovate should use `minimumReleaseAge = "7 days"` so dependency PRs do not fight package-manager cooldowns. -Security workflows should include secret scanning and dependency review for pull requests. +Security workflows are opt-in extras: + +- Add `extras/github/gitleaks.yml.example` only when maintainers want CI history + scanning for secrets and have a plan to handle baseline findings. +- Add `extras/github/dependency-review.yml.example` only after confirming the + target repo uses GitHub's dependency graph and wants known-vulnerability, + license, or dependency-change reporting. + +Cooldowns, locked installs, committed lockfiles, and least-privilege CI remain +the default supply-chain defenses. diff --git a/docs/template-proposal.md b/docs/template-proposal.md index 77eafb1..afd4b70 100644 --- a/docs/template-proposal.md +++ b/docs/template-proposal.md @@ -7,6 +7,7 @@ This template captures recurring conventions from existing repositories so codin It is optimized for: - Codex: `AGENTS.md`, `.context/`, explicit scripts, surgical edit guidance. +- Conductor: gitignored `.context/` for workspace-local agent scratch. - Claude Code: `CLAUDE.md` as a short pointer to canonical rules. - Cursor: `.cursor/rules/repo-conventions.mdc`. - Future agents: documented boundaries, deterministic commands, and machine-readable structure. @@ -18,26 +19,24 @@ It is optimized for: ├── AGENTS.md ├── CLAUDE.md ├── README.md -├── .context/ -│ ├── architecture/ -│ ├── decisions/ -│ └── runbooks/ ├── .cursor/rules/ ├── .github/workflows/ -├── apps/ -│ ├── api/ -│ └── web/ -├── packages/ -│ └── shared/ +├── stacks/ +│ ├── python/ +│ ├── typescript/ +│ ├── go/ +│ └── rust/ +├── extras/ ├── docs/ └── scripts/ ``` ## Defaults -- Python: `uv`, Ruff, MyPy, Pytest. -- JavaScript: Bun, Biome, TypeScript, Vitest. -- Infra: Docker Compose for Postgres and Redis. +- Repository tooling: Bun, Biome, TypeScript, Vitest. +- Optional Python stack: `uv`, Ruff, MyPy, Pytest. +- Infra: Docker Compose examples for local databases, caches, or similar + services. - Local dev: host-run app services with Docker-managed infra. - Ports: stable worktree-derived allocations. - CI: frozen installs, area-aware checks, lint/type/test parity. @@ -45,9 +44,10 @@ It is optimized for: ## Alternative Paths -- For very large JS monorepos, pnpm is still an acceptable fallback. -- For static sites, drop `apps/api`, Postgres, and Redis. -- For Python-only repos, drop pnpm workspace files and web CI. +- For Python APIs, workers, or shared packages, copy `stacks/python/`. +- For Go, Rust, or other runtimes, add matching `stacks//` directories instead of changing the root base. +- For very large JS monorepos, pnpm is a first-class option. +- For static sites, drop database and cache services. - For product repos with browser UI, add Playwright after the first interactive flow exists. - For LLM features, add deterministic eval fixtures before live model evals. diff --git a/docs/tooling.md b/docs/tooling.md new file mode 100644 index 0000000..9a708a0 --- /dev/null +++ b/docs/tooling.md @@ -0,0 +1,70 @@ +# Tooling + +The root devkit is language-neutral. Language and runtime conventions live in +`stacks/`, and optional workflow or deployment add-ons live in `extras/`. + +## TypeScript Stack + +Show Bun first for TypeScript repository tooling. It keeps scripts fast and +simple, but pnpm is also first-class when a repo or team prefers it. + +Required checks for `stacks/typescript`: + +```bash +bun run lint +bun run typecheck +bun run test +bun run build +``` + +Use pnpm when a workspace or team wants its monorepo tooling, workspace +controls, or ecosystem compatibility. The pnpm convention files live in +`stacks/typescript/pnpm/`. + +## Python Stack + +Use `uv` for Python installs and execution when the target project selects +`stacks/python`. + +Required checks for `stacks/python`: + +```bash +uv sync --locked +UV_LOCKED=1 uv run ruff check apps packages tests +UV_LOCKED=1 uv run ruff format --check apps packages tests +UV_LOCKED=1 uv run mypy +UV_LOCKED=1 uv run pytest +``` + +Keep Python configuration in `pyproject.toml`. The Python stack shows Pydantic +settings/boundary schemas and Alembic migrations as examples; keep them when +they fit the target repo and replace them when an existing choice is better. + +## Dependency Safety + +Use dependency cooldowns and frozen installs: + +- Bun: `bunfig.toml` sets `minimumReleaseAge = 604800` seconds. +- uv: optional `exclude-newer = "P7D"` only with uv `0.9.17` or newer. Agents + must check `uv --no-config --version` before writing relative cooldowns + downstream and ask before upgrading old uv installations. +- pnpm: `pnpm-workspace.yaml` should set `minimumReleaseAge: 10080` minutes. +- CI should use locked or frozen installs. + +Regenerate lockfiles when cooldown settings change so CI validates committed +pins instead of resolving fresh dependency graphs. + +## Workflow Permissions + +Keep workflow permissions at `contents: read` unless a job explicitly calls PR +APIs or posts PR comments. + +Dependency Review is opt-in through +`extras/github/dependency-review.yml.example`, not a default hidden behind a +repository variable. Do not make it run automatically for all public +repositories with `github.event.repository.private == false`; that reintroduces +the dependency graph footgun for public repos where the graph is disabled. + +Gitleaks is also opt-in through `extras/github/gitleaks.yml.example`. Add it +only when maintainers want CI secret scanning and are prepared to triage +historic findings. diff --git a/extras/dev-scripts/README.md b/extras/dev-scripts/README.md new file mode 100644 index 0000000..a3c4854 --- /dev/null +++ b/extras/dev-scripts/README.md @@ -0,0 +1,26 @@ +# Dev Script Extras + +The root template keeps shell wrappers as the canonical entrypoints: + +- `scripts/dev.sh` +- `scripts/docker-compose.sh` +- `scripts/check-all.sh` + +That is intentional. Shell wrappers are easy for humans, CI, and agents to discover, and they can delegate to Python or Bun where those tools are better. + +Recommended split: + +- Use `.sh` for stable top-level commands and process orchestration. +- Use shell for dependency-free deterministic logic that must work before language dependencies are installed, such as worktree ports. +- Use `.mjs` or `.ts` for JS-only projects where the script directly interacts with Vite, Next.js, Drizzle, or TypeScript config. + +This directory provides examples for JS-first repos that want JavaScript or TypeScript helper scripts. + +## Agent Notes + +- Keep `.sh` wrappers as the top-level interface unless the target repo has a + strong reason to standardize on another script runtime. +- Use these examples as implementation references, not as files to copy into + every project. +- If a helper depends on Bun, Node, or TypeScript, keep that dependency inside + the selected stack instead of making root setup language-specific. diff --git a/alternates/dev-scripts/dev.ts b/extras/dev-scripts/dev.ts similarity index 92% rename from alternates/dev-scripts/dev.ts rename to extras/dev-scripts/dev.ts index 056c0c8..399b24d 100644 --- a/alternates/dev-scripts/dev.ts +++ b/extras/dev-scripts/dev.ts @@ -35,7 +35,7 @@ const api = run("api", "uv", [ "--reload", ]); -const web = run("web", "bun", ["run", "--cwd", "apps/web", "dev"]); +const web = run("web", "bun", ["run", "--cwd", "stacks/typescript", "dev"]); process.on("SIGINT", () => { api.kill("SIGTERM"); diff --git a/alternates/dev-scripts/worktree-ports.mjs b/extras/dev-scripts/worktree-ports.mjs similarity index 100% rename from alternates/dev-scripts/worktree-ports.mjs rename to extras/dev-scripts/worktree-ports.mjs index ba49a7a..0192ccf 100755 --- a/alternates/dev-scripts/worktree-ports.mjs +++ b/extras/dev-scripts/worktree-ports.mjs @@ -1,6 +1,6 @@ #!/usr/bin/env node -import { createHash } from "node:crypto"; import { execFileSync } from "node:child_process"; +import { createHash } from "node:crypto"; import { resolve } from "node:path"; const BASE_PORT = 8700; diff --git a/extras/devcontainer/README.md b/extras/devcontainer/README.md new file mode 100644 index 0000000..ee7cb89 --- /dev/null +++ b/extras/devcontainer/README.md @@ -0,0 +1,13 @@ +# Dev Container Extra + +Use this extra only when the team wants a containerized editor/runtime environment. + +It is not a root default because many 508.dev repos prefer host-run app services for reload speed and agent debuggability. + +Copy `devcontainer.json.example` to `.devcontainer/devcontainer.json` and adapt ports, extensions, and post-create commands for the target repo. + +## Agent Notes + +- Do not add a dev container just because this extra exists. +- Check whether the team prefers host-run services first. +- Adapt extensions, ports, and post-create commands to the selected stacks. diff --git a/alternates/devcontainer/devcontainer.json.example b/extras/devcontainer/devcontainer.json.example similarity index 100% rename from alternates/devcontainer/devcontainer.json.example rename to extras/devcontainer/devcontainer.json.example diff --git a/alternates/dockerfiles/Dockerfile.api.example b/extras/dockerfiles/Dockerfile.api.example similarity index 100% rename from alternates/dockerfiles/Dockerfile.api.example rename to extras/dockerfiles/Dockerfile.api.example diff --git a/alternates/dockerfiles/Dockerfile.web-typescript.example b/extras/dockerfiles/Dockerfile.web-typescript.example similarity index 61% rename from alternates/dockerfiles/Dockerfile.web-typescript.example rename to extras/dockerfiles/Dockerfile.web-typescript.example index a32c7e8..95241c3 100644 --- a/alternates/dockerfiles/Dockerfile.web-typescript.example +++ b/extras/dockerfiles/Dockerfile.web-typescript.example @@ -3,7 +3,7 @@ FROM oven/bun:1.3.13 AS build WORKDIR /app COPY package.json bun.lock bunfig.toml biome.json ./ -COPY apps/web apps/web +COPY stacks/typescript stacks/typescript RUN bun install --frozen-lockfile -RUN bun run --cwd apps/web build +RUN bun run --cwd stacks/typescript build diff --git a/alternates/dockerfiles/Dockerfile.worker.example b/extras/dockerfiles/Dockerfile.worker.example similarity index 100% rename from alternates/dockerfiles/Dockerfile.worker.example rename to extras/dockerfiles/Dockerfile.worker.example diff --git a/alternates/dockerfiles/README.md b/extras/dockerfiles/README.md similarity index 65% rename from alternates/dockerfiles/README.md rename to extras/dockerfiles/README.md index 1804ba8..23f90c9 100644 --- a/alternates/dockerfiles/README.md +++ b/extras/dockerfiles/README.md @@ -11,3 +11,11 @@ Copy and adapt only after choosing the service boundary and deployment target. - `Dockerfile.web-typescript.example`: framework-neutral TypeScript check/build image. For framework-specific web deploys, replace the web example with the framework's production build and runtime guidance. + +## Agent Notes + +- Do not add Dockerfiles until the deployment target and service boundaries are + known. +- Keep build contexts small and confirm `.dockerignore` excludes secrets, + caches, local dependencies, and `.context/`. +- Replace example package names and commands with target-specific entrypoints. diff --git a/alternates/github/CODEOWNERS.example b/extras/github/CODEOWNERS.example similarity index 100% rename from alternates/github/CODEOWNERS.example rename to extras/github/CODEOWNERS.example diff --git a/extras/github/README.md b/extras/github/README.md new file mode 100644 index 0000000..2d8d1e4 --- /dev/null +++ b/extras/github/README.md @@ -0,0 +1,37 @@ +# GitHub Extras + +These files are useful in many repositories but should be copied intentionally. + +## Agent Notes + +- Do not copy every file in this directory by default. +- Ask what governance or security workflow the target repo actually wants. +- Prefer explaining an extra as available over silently adding a workflow that + can create noisy checks or require repository settings. + +## CODEOWNERS + +`CODEOWNERS.example` is a starting point for review ownership. Do not commit it as active `.github/CODEOWNERS` until the owners are real GitHub users or teams. + +Active CODEOWNERS affects reviewer routing and may interact with branch protection, so keep ownership broad at first and tighten it as the team stabilizes. + +## Community Templates + +Use `community/DISCUSSION_TEMPLATE/questions.yml` only for repositories that use GitHub Discussions for public or internal support. Keep the language project-specific and avoid making the template longer than the support workflow can justify. + +## Secret Scanning + +Use `gitleaks.yml.example` when maintainers want CI history scanning for +secrets. It downloads a pinned gitleaks release, verifies the checksum, and runs +`gitleaks git`. + +Do not add it automatically to every repository. Secret scanners can surface +historic findings or false positives that need a cleanup process. + +## Dependency Review + +Use `dependency-review.yml.example` when maintainers want GitHub dependency +graph-based vulnerability, license, or dependency-change reporting. + +Do not add it automatically. It requires GitHub's dependency graph to be enabled +and is not the primary active supply-chain attack detector. diff --git a/alternates/github/community/DISCUSSION_TEMPLATE/questions.yml b/extras/github/community/DISCUSSION_TEMPLATE/questions.yml similarity index 100% rename from alternates/github/community/DISCUSSION_TEMPLATE/questions.yml rename to extras/github/community/DISCUSSION_TEMPLATE/questions.yml diff --git a/extras/github/dependency-review.yml.example b/extras/github/dependency-review.yml.example new file mode 100644 index 0000000..968a004 --- /dev/null +++ b/extras/github/dependency-review.yml.example @@ -0,0 +1,26 @@ +name: Dependency Review + +on: + pull_request: + workflow_dispatch: + +permissions: + contents: read + +jobs: + dependency-review: + runs-on: ubuntu-latest + steps: + - uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2 + with: + egress-policy: audit + + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 + with: + persist-credentials: false + + - name: Dependency Review + # This is an opt-in dependency-graph report gate. Do not copy it into a + # repo unless GitHub's dependency graph is enabled and the owner wants + # vulnerability/license/dependency-change reporting. + uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0 diff --git a/.github/workflows/security.yml b/extras/github/gitleaks.yml.example similarity index 60% rename from .github/workflows/security.yml rename to extras/github/gitleaks.yml.example index cf5e86d..70bd364 100644 --- a/.github/workflows/security.yml +++ b/extras/github/gitleaks.yml.example @@ -1,4 +1,4 @@ -name: Security +name: Secret Scan on: pull_request: @@ -9,7 +9,6 @@ on: permissions: contents: read - pull-requests: read jobs: secrets: @@ -21,14 +20,21 @@ jobs: - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 with: + # Gitleaks history scanning needs full git history. Keep credentials + # non-persistent because no workflow step pushes back to the repo. fetch-depth: 0 persist-credentials: false - name: Gitleaks + # Opt-in secret scanning. Copy this workflow only when the target repo + # wants history scanning in CI and maintainers are ready to handle + # baseline findings or false positives. env: GITLEAKS_VERSION: 8.30.1 run: | set -euo pipefail + # Download and verify the pinned release archive instead of executing + # a moving installer script. archive="gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" checksums="gitleaks_${GITLEAKS_VERSION}_checksums.txt" base_url="https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}" @@ -36,19 +42,4 @@ jobs: curl --fail --location --show-error --silent --remote-name "${base_url}/${checksums}" grep " ${archive}$" "${checksums}" | sha256sum --check - tar -xzf "${archive}" gitleaks - ./gitleaks detect --source . --redact --no-banner --verbose - - dependency-review: - if: github.event_name == 'pull_request' && (github.event.repository.private == false || vars.ENABLE_PRIVATE_DEPENDENCY_REVIEW == 'true') - runs-on: ubuntu-latest - steps: - - uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2 - with: - egress-policy: audit - - - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 - with: - persist-credentials: false - - - name: Dependency Review - uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0 + ./gitleaks git . --redact --no-banner --verbose diff --git a/extras/object-storage/README.md b/extras/object-storage/README.md new file mode 100644 index 0000000..5c7402c --- /dev/null +++ b/extras/object-storage/README.md @@ -0,0 +1,43 @@ +# Object Storage Extra + +Use this extra only when a project needs local S3-compatible object storage. +Object storage is not a root default because many projects do not need it, and +the right provider or emulator depends on the product. + +## Contains + +- `compose.object-storage.yml.example`: opt-in MinIO services for Docker + Compose, based on the 508-workflows local infrastructure pattern. + +## MinIO Notes + +The example uses Chainguard images: + +- `cgr.dev/chainguard/minio` for the server. +- `cgr.dev/chainguard/minio-client` for one-time bucket creation. + +Keep the server and client images explicit. Official and Chainguard MinIO image +contents and entrypoints can differ, and prior downstream work hit issues when +assuming one image could safely cover both server runtime and client setup. + +The `minio-init` service waits for the server healthcheck and runs: + +```bash +mc mb --ignore-existing local/${MINIO_INTERNAL_BUCKET:-internal-transfers} +``` + +This keeps bucket creation idempotent and makes app services depend on +`minio-init` completing successfully instead of racing the first upload. + +## Agent Notes + +- Do not add this extra unless the target repo actually needs object storage. +- Prefer an explicit bucket-init service over ad hoc app startup bucket + creation. +- Use `MINIO_ROOT_USER` and `MINIO_ROOT_PASSWORD` as env-loaded fields. + `MINIO_ACCESS_KEY` and `MINIO_SECRET_KEY` may be application aliases, but do + not assume MinIO itself reads those names. +- Add worktree-safe host ports only when the service is exposed to the host. + Internal Compose consumers should use `http://minio:9000`. +- If copying this into a repo with existing object storage config, adapt names + and bucket policy to the repo instead of preserving `internal-transfers`. diff --git a/extras/object-storage/compose.object-storage.yml.example b/extras/object-storage/compose.object-storage.yml.example new file mode 100644 index 0000000..2b1fca8 --- /dev/null +++ b/extras/object-storage/compose.object-storage.yml.example @@ -0,0 +1,36 @@ +services: + minio: + image: cgr.dev/chainguard/minio + command: ["server", "/data", "--console-address", ":9001"] + environment: + MINIO_ROOT_USER: ${MINIO_ROOT_USER:-internal} + MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:-change-me} + # The server healthcheck runs inside the MinIO container, so localhost is + # the right endpoint for checking this service from itself. + MC_HOST_local: "http://${MINIO_ROOT_USER:-internal}:${MINIO_ROOT_PASSWORD:-change-me}@127.0.0.1:9000" + restart: unless-stopped + volumes: + - minio-data:/data + healthcheck: + test: ["CMD-SHELL", "mc ready local || exit 1"] + interval: 10s + timeout: 3s + retries: 5 + + minio-init: + image: cgr.dev/chainguard/minio-client + environment: + # The init service runs in a separate container, so use Compose service + # DNS instead of localhost. + MC_HOST_local: "http://${MINIO_ROOT_USER:-internal}:${MINIO_ROOT_PASSWORD:-change-me}@minio:9000" + command: + - mb + - --ignore-existing + - local/${MINIO_INTERNAL_BUCKET:-internal-transfers} + depends_on: + minio: + condition: service_healthy + restart: "no" + +volumes: + minio-data: diff --git a/alternates/todo-to-issue/README.md b/extras/todo-to-issue/README.md similarity index 69% rename from alternates/todo-to-issue/README.md rename to extras/todo-to-issue/README.md index d0a9a06..3b9a7c9 100644 --- a/alternates/todo-to-issue/README.md +++ b/extras/todo-to-issue/README.md @@ -1,6 +1,6 @@ # TODO To Issue Workflow -This alternate converts TODO-style comments into GitHub issues. It is intentionally not enabled by default. +This extra converts TODO-style comments into GitHub issues. It is intentionally not enabled by default. Reasons to opt in carefully: @@ -18,3 +18,10 @@ Before enabling it: 5. Prefer manual `workflow_dispatch` at first, then move to scheduled or merge-triggered runs after the workflow proves useful. Copy `todo-to-issue.yml.example` to `.github/workflows/todo-to-issue.yml` when ready. + +## Agent Notes + +- Do not enable this workflow without an explicit TODO policy. +- Prefer `workflow_dispatch` while testing so the repo does not create issue + noise automatically. +- Re-check workflow permissions before copying it into a target repo. diff --git a/alternates/todo-to-issue/todo-to-issue.yml.example b/extras/todo-to-issue/todo-to-issue.yml.example similarity index 100% rename from alternates/todo-to-issue/todo-to-issue.yml.example rename to extras/todo-to-issue/todo-to-issue.yml.example diff --git a/llms.txt b/llms.txt index 9770189..a2190a9 100644 --- a/llms.txt +++ b/llms.txt @@ -1,6 +1,6 @@ # 508 Devkit -508 Devkit is an opinionated reference repo of sane defaults and conventions for 508.dev projects. +508 Devkit is an opinionated reference repo of sane defaults and conventions for software projects. It comes from 508.dev practice, but it is meant to be useful outside 508.dev too. ## Start Here @@ -14,13 +14,16 @@ ## Key Conventions -- Use `uv` for Python workspaces. -- Use Bun for JavaScript package management unless the target repo needs pnpm. +- Use `uv` for Python workspaces when the Python stack is selected. +- Check `uv --no-config --version` before adding `exclude-newer = "P7D"`; ask + before upgrading uv if the target has a version older than `0.9.17`. +- Show Bun first for JavaScript examples, but treat pnpm as first-class when a + repo or team prefers it. - Keep frontend framework choice out of root defaults. - Run app services on the host and infrastructure in Docker Compose. -- Use deterministic worktree ports from `scripts/worktree-ports.py`. -- Keep `.context/` for operational memory, not user-facing docs. -- Put opt-in or team-specific choices in `alternates/`. +- Use deterministic worktree ports from `scripts/worktree-ports.sh`. +- Keep `.context/` gitignored and workspace-local; durable knowledge belongs in tracked docs. +- Put language/runtime conventions in `stacks/` and opt-in add-ons in `extras/`. ## Validation diff --git a/package.json b/package.json index d4bfa66..0804b25 100644 --- a/package.json +++ b/package.json @@ -5,21 +5,21 @@ "type": "module", "packageManager": "bun@1.3.13", "workspaces": [ - "apps/web" + "stacks/typescript" ], "scripts": { "dev": "./scripts/dev.sh", - "build": "bun run --cwd apps/web build", - "lint": "biome lint .", + "build": "bun run --cwd stacks/typescript build", + "lint": "biome check .", "format": "biome format --write .", "format:check": "biome format .", - "typecheck": "bun run --cwd apps/web typecheck", - "test": "bun run --cwd apps/web test", + "typecheck": "bun run --cwd stacks/typescript typecheck", + "test": "bun run --cwd stacks/typescript test", "check": "./scripts/check-all.sh", - "ports": "python3 scripts/worktree-ports.py env", - "db:generate": "bun run --cwd apps/web db:generate", - "db:push": "bun run --cwd apps/web db:push", - "db:studio": "bun run --cwd apps/web db:studio" + "ports": "./scripts/worktree-ports.sh env", + "db:generate": "bun run --cwd stacks/typescript db:generate", + "db:push": "bun run --cwd stacks/typescript db:push", + "db:studio": "bun run --cwd stacks/typescript db:studio" }, "devDependencies": { "@biomejs/biome": "^2.4.13", diff --git a/pnpm-workspace.example.yaml b/pnpm-workspace.example.yaml index 31afa35..8cb66d1 100644 --- a/pnpm-workspace.example.yaml +++ b/pnpm-workspace.example.yaml @@ -1,7 +1,7 @@ -# Optional pnpm fallback for large JavaScript workspaces. -# The default template uses Bun. +# Optional pnpm setup for JavaScript workspaces that prefer pnpm. +# The root TypeScript example shows Bun first, but pnpm is first-class too. packages: - - "apps/web" + - "stacks/typescript" # Seven days, in minutes. pnpm v10.16+ supports this setting here. minimumReleaseAge: 10080 diff --git a/scripts/check-all.sh b/scripts/check-all.sh index 494cea9..e994ad9 100755 --- a/scripts/check-all.sh +++ b/scripts/check-all.sh @@ -1,8 +1,11 @@ #!/usr/bin/env sh set -eu +cd "$(dirname "$0")/.." + +# Root validation covers the language-neutral wrapper scripts plus the default +# TypeScript stack. Run stack-local check-all scripts after selecting extras. ./scripts/lint.sh -uv run ruff format --check apps packages tests ./scripts/typecheck.sh ./scripts/test.sh bun run build diff --git a/scripts/dev.sh b/scripts/dev.sh index 97e9460..de5a11b 100755 --- a/scripts/dev.sh +++ b/scripts/dev.sh @@ -3,13 +3,13 @@ set -eu cd "$(dirname "$0")/.." -eval "$(python3 scripts/worktree-ports.py export)" -export API_HOST="${API_HOST:-127.0.0.1}" +eval "$(./scripts/worktree-ports.sh export)" export WEB_HOST="${WEB_HOST:-127.0.0.1}" -export PYTHONPATH="${PYTHONPATH:-apps/api/src:packages/shared/src}" +# Root dev runs only language-neutral infra plus the TypeScript convention +# watcher. Runtime-specific services, such as the Python API, live in stacks and +# should be started from their stack scripts when selected for a target repo. echo "508 Devkit local stack" -echo " API: http://${API_HOST}:${API_PORT}" echo " Web: framework-neutral TypeScript watcher; reserve WEB_PORT=${WEB_PORT} for your chosen web framework" echo " Postgres: 127.0.0.1:${POSTGRES_HOST_PORT}" echo " Redis: 127.0.0.1:${REDIS_HOST_PORT}" @@ -17,22 +17,52 @@ echo ./scripts/docker-compose.sh up -d postgres redis +detect_js_runner() { + if [ -n "${DEVKIT_JS_RUNNER:-}" ]; then + printf '%s\n' "$DEVKIT_JS_RUNNER" + return + fi + + # Keep the root script usable when a repository chooses the pnpm stack + # variant. The packageManager field is the strongest signal; lockfiles are a + # fallback for copied templates where package.json has been edited. + if grep -Eq '"packageManager"[[:space:]]*:[[:space:]]*"pnpm@' package.json 2>/dev/null; then + printf '%s\n' pnpm + return + fi + + if grep -Eq '"packageManager"[[:space:]]*:[[:space:]]*"bun@' package.json 2>/dev/null; then + printf '%s\n' bun + return + fi + + if [ -f pnpm-lock.yaml ]; then + printf '%s\n' pnpm + return + fi + + printf '%s\n' bun +} + +JS_RUNNER="$(detect_js_runner)" + cleanup() { - if [ -n "${API_PID:-}" ]; then kill "$API_PID" 2>/dev/null || true; fi if [ -n "${WEB_PID:-}" ]; then kill "$WEB_PID" 2>/dev/null || true; fi } trap cleanup INT TERM EXIT -uv run --package example-api uvicorn example_api.main:create_app \ - --factory \ - --host "$API_HOST" \ - --port "$API_PORT" \ - --reload \ - --reload-dir apps/api/src \ - --reload-dir packages/shared/src & -API_PID=$! - -bun run --cwd apps/web dev & +case "$JS_RUNNER" in + bun) + bun run --cwd stacks/typescript dev & + ;; + pnpm) + pnpm -C stacks/typescript run dev & + ;; + *) + echo "Unsupported DEVKIT_JS_RUNNER=${JS_RUNNER}; expected bun or pnpm." >&2 + exit 1 + ;; +esac WEB_PID=$! -wait "$API_PID" "$WEB_PID" +wait "$WEB_PID" diff --git a/scripts/docker-compose.sh b/scripts/docker-compose.sh index 3483b6b..b0b6f8b 100755 --- a/scripts/docker-compose.sh +++ b/scripts/docker-compose.sh @@ -3,15 +3,45 @@ set -eu cd "$(dirname "$0")/.." +# Prefer developer-provided .env values, but keep .env.example as the baseline +# so Compose validation works before a local .env exists. ENV_FILE=".env" if [ ! -f "$ENV_FILE" ]; then ENV_FILE=".env.example" fi +load_port_reservations() { + file="$1" + if [ ! -f "$file" ]; then + return 0 + fi + + # Read only the reservation inputs consumed by worktree-ports.sh. Avoid + # sourcing the whole .env file because local env files are configuration, not + # shell scripts. + while IFS= read -r line || [ -n "$line" ]; do + case "$line" in + WORKTREE_PORT_BLOCK_START=*|WORKTREE_PORT_BLOCK_SIZE=*|WORKTREE_PRIMARY_PORT=*|WORKTREE_PRIMARY_PORT_TARGET=*) + key="${line%%=*}" + value="${line#*=}" + case "$value" in + \"*\") value="${value#\"}"; value="${value%\"}" ;; + \'*\') value="${value#\'}"; value="${value%\'}" ;; + esac + export "$key=$value" + ;; + esac + done < "$file" +} + +load_port_reservations "$ENV_FILE" + PORT_ENV_FILE="$(mktemp)" trap 'rm -f "$PORT_ENV_FILE"' EXIT HUP INT TERM -python3 scripts/worktree-ports.py env > "$PORT_ENV_FILE" +./scripts/worktree-ports.sh env > "$PORT_ENV_FILE" +# Env-file order is significant: examples provide defaults, generated ports +# make sibling worktrees safe, and .env has final local override authority. if [ "$ENV_FILE" = ".env" ]; then exec docker compose -f compose.yml --env-file .env.example --env-file "$PORT_ENV_FILE" --env-file .env "$@" fi diff --git a/scripts/format.sh b/scripts/format.sh index 534e5b8..889b14c 100755 --- a/scripts/format.sh +++ b/scripts/format.sh @@ -1,5 +1,7 @@ #!/usr/bin/env sh set -eu -uv run ruff format apps packages tests +cd "$(dirname "$0")/.." + +# Formatting is explicit and separate from format:check/pre-commit. bun run format diff --git a/scripts/lint.sh b/scripts/lint.sh index 1735b6e..214ef57 100755 --- a/scripts/lint.sh +++ b/scripts/lint.sh @@ -1,5 +1,8 @@ #!/usr/bin/env sh set -eu -uv run ruff check apps packages tests +cd "$(dirname "$0")/.." + +# Delegate to package scripts so target repos can swap tools without changing +# every shell entrypoint. bun run lint diff --git a/scripts/test.sh b/scripts/test.sh index 10b611c..0d137b9 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -1,5 +1,7 @@ #!/usr/bin/env sh set -eu -uv run pytest +cd "$(dirname "$0")/.." + +# Keep tests behind a stable wrapper for humans, agents, and CI. bun run test diff --git a/scripts/typecheck.sh b/scripts/typecheck.sh index f2e0847..851c59a 100755 --- a/scripts/typecheck.sh +++ b/scripts/typecheck.sh @@ -1,5 +1,7 @@ #!/usr/bin/env sh set -eu -uv run mypy +cd "$(dirname "$0")/.." + +# Keep the root command stable while the TypeScript stack owns compiler details. bun run typecheck diff --git a/scripts/worktree-ports.sh b/scripts/worktree-ports.sh new file mode 100755 index 0000000..8ed11ce --- /dev/null +++ b/scripts/worktree-ports.sh @@ -0,0 +1,309 @@ +#!/usr/bin/env sh +set -eu + +BASE_PORT=8700 +SPAN=1000 +PORT_BLOCK_SIZE=100 +RESERVED_BLOCK_SIZE_DEFAULT=10 + +# Keep this list in sync with browser-restricted local ports. The API is also +# browser-facing because web apps often call it with fetch(), so both API_PORT +# and WEB_PORT are sanitized through chrome_safe_port(). +WEB_RESTRICTED_PORTS=" 1 7 9 11 13 15 17 19 20 21 22 23 25 37 42 43 53 69 77 79 87 95 101 102 103 104 109 110 111 113 115 117 119 123 135 137 139 143 161 179 389 427 465 512 513 514 515 526 530 531 532 540 548 554 556 563 587 601 636 989 990 993 995 1719 1720 1723 2049 3659 4045 5060 5061 6000 6566 6665 6666 6667 6668 6669 6697 10080 " + +usage() { + echo "usage: worktree-ports.sh [env|export|exec [KEY=VALUE ...] -- ]" >&2 +} + +worktree_root() { + if root="$(git rev-parse --show-toplevel 2>/dev/null)"; then + printf '%s\n' "$root" + else + pwd -P + fi +} + +hash_hex() { + root="$1" + # macOS ships shasum, Linux commonly ships sha256sum, and openssl is a + # practical fallback. Avoid Python/Node here so root scripts stay + # language-neutral before any stack-specific dependencies are installed. + if command -v shasum >/dev/null 2>&1; then + printf '%s' "$root" | shasum -a 256 | awk '{print $1}' + elif command -v sha256sum >/dev/null 2>&1; then + printf '%s' "$root" | sha256sum | awk '{print $1}' + elif command -v openssl >/dev/null 2>&1; then + printf '%s' "$root" | openssl dgst -sha256 | awk '{print $NF}' + else + echo "worktree-ports.sh requires shasum, sha256sum, or openssl" >&2 + return 1 + fi +} + +hex_to_decimal() { + # POSIX shell arithmetic does not portably parse 32-bit hex values, so awk + # does the conversion in a way that works on macOS and Linux. + awk -v hex="$1" ' + BEGIN { + decimal = 0 + digits = "0123456789abcdef" + hex = tolower(hex) + for (pos = 1; pos <= length(hex); pos++) { + value = index(digits, substr(hex, pos, 1)) - 1 + decimal = (decimal * 16) + value + } + print decimal + } + ' +} + +port_block() { + # Hash the absolute worktree path so sibling git worktrees receive stable but + # different port blocks without a central registry. + root="$(worktree_root)" + digest="$(hash_hex "$root")" + prefix="$(printf '%s' "$digest" | cut -c 1-8)" + value="$(hex_to_decimal "$prefix")" + blocks=$((SPAN / PORT_BLOCK_SIZE)) + echo $((BASE_PORT + ((value % blocks) * PORT_BLOCK_SIZE))) +} + +is_positive_integer() { + case "${1:-}" in + ''|*[!0-9]*) return 1 ;; + *) [ "$1" -gt 0 ] ;; + esac +} + +reserved_block_start() { + if is_positive_integer "${WORKTREE_PORT_BLOCK_START:-}"; then + printf '%s\n' "$WORKTREE_PORT_BLOCK_START" + return 0 + fi + return 1 +} + +reserved_block_size() { + if is_positive_integer "${WORKTREE_PORT_BLOCK_SIZE:-}"; then + printf '%s\n' "$WORKTREE_PORT_BLOCK_SIZE" + else + printf '%s\n' "$RESERVED_BLOCK_SIZE_DEFAULT" + fi +} + +is_web_restricted_port() { + case "$WEB_RESTRICTED_PORTS" in + *" $1 "*) return 0 ;; + *) return 1 ;; + esac +} + +chrome_safe_port() { + port="$1" + while is_web_restricted_port "$port"; do + port=$((port + 1)) + done + echo "$port" +} + +is_used_port() { + case "$2" in + *" $1 "*) return 0 ;; + *) return 1 ;; + esac +} + +unused_port_in_block() { + port="$1" + end="$2" + used="$3" + while [ "$port" -le "$end" ]; do + if ! is_used_port "$port" "$used"; then + echo "$port" + return 0 + fi + port=$((port + 1)) + done + echo "reserved port block does not have enough free ports" >&2 + return 1 +} + +browser_safe_unused_port_in_block() { + port="$1" + end="$2" + used="$3" + while [ "$port" -le "$end" ]; do + if ! is_web_restricted_port "$port" && ! is_used_port "$port" "$used"; then + echo "$port" + return 0 + fi + port=$((port + 1)) + done + echo "reserved port block does not have enough browser-safe free ports" >&2 + return 1 +} + +apply_primary_port_reservation() { + if ! is_positive_integer "${WORKTREE_PRIMARY_PORT:-}"; then + return 0 + fi + + primary="$(chrome_safe_port "$WORKTREE_PRIMARY_PORT")" + target="${WORKTREE_PRIMARY_PORT_TARGET:-WEB_PORT}" + case "$target" in + API_PORT) + API_PORT="$primary" + ;; + WEB_PORT) + WEB_PORT="$primary" + ;; + *) + echo "WORKTREE_PRIMARY_PORT_TARGET must be API_PORT or WEB_PORT" >&2 + return 1 + ;; + esac +} + +validate_unique_ports() { + used=" " + for port in "$API_PORT" "$WEB_PORT" "$WORKER_HEALTH_PORT" "$POSTGRES_HOST_PORT" "$REDIS_HOST_PORT" "$OTEL_HTTP_PORT"; do + if is_used_port "$port" "$used"; then + echo "worktree port reservation produced duplicate port ${port}; adjust WORKTREE_PRIMARY_PORT, WORKTREE_PRIMARY_PORT_TARGET, or WORKTREE_PORT_BLOCK_*" >&2 + return 1 + fi + used="${used}${port} " + done +} + +calculate_ports() { + if base="$(reserved_block_start)"; then + size="$(reserved_block_size)" + if [ "$size" -lt 6 ]; then + echo "WORKTREE_PORT_BLOCK_SIZE must be at least 6" >&2 + return 1 + fi + # A reserved block is usually small, so use compact offsets inside it. + end=$((base + size - 1)) + used=" " + WEB_PORT="$(browser_safe_unused_port_in_block "$base" "$end" "$used")" + used="${used}${WEB_PORT} " + API_PORT="$(browser_safe_unused_port_in_block "$((base + 1))" "$end" "$used")" + used="${used}${API_PORT} " + WORKER_HEALTH_PORT="$(unused_port_in_block "$((base + 2))" "$end" "$used")" + used="${used}${WORKER_HEALTH_PORT} " + POSTGRES_HOST_PORT="$(unused_port_in_block "$((base + 3))" "$end" "$used")" + used="${used}${POSTGRES_HOST_PORT} " + REDIS_HOST_PORT="$(unused_port_in_block "$((base + 4))" "$end" "$used")" + used="${used}${REDIS_HOST_PORT} " + OTEL_HTTP_PORT="$(unused_port_in_block "$((base + 5))" "$end" "$used")" + else + base="$(port_block)" + # Offsets are intentionally sparse. Future services can claim unused slots + # without changing existing ports for API, web, database, or cache examples. + API_PORT="$(chrome_safe_port "$((base + 20))")" + WEB_PORT="$(chrome_safe_port "$((base + 30))")" + WORKER_HEALTH_PORT="$((base + 35))" + POSTGRES_HOST_PORT="$((base + 40))" + REDIS_HOST_PORT="$((base + 50))" + OTEL_HTTP_PORT="$((base + 80))" + fi + + apply_primary_port_reservation + validate_unique_ports + + POSTGRES_URL="postgresql://app:app@127.0.0.1:${POSTGRES_HOST_PORT}/app" + DATABASE_URL="$POSTGRES_URL" + REDIS_URL="redis://127.0.0.1:${REDIS_HOST_PORT}/0" + WEB_API_BASE_URL="http://127.0.0.1:${API_PORT}" + OTEL_EXPORTER_OTLP_ENDPOINT="http://127.0.0.1:${OTEL_HTTP_PORT}" +} + +print_env() { + prefix="$1" + calculate_ports + printf '%sAPI_PORT=%s\n' "$prefix" "$API_PORT" + printf '%sWEB_PORT=%s\n' "$prefix" "$WEB_PORT" + printf '%sWORKER_HEALTH_PORT=%s\n' "$prefix" "$WORKER_HEALTH_PORT" + printf '%sPOSTGRES_HOST_PORT=%s\n' "$prefix" "$POSTGRES_HOST_PORT" + printf '%sREDIS_HOST_PORT=%s\n' "$prefix" "$REDIS_HOST_PORT" + printf '%sOTEL_HTTP_PORT=%s\n' "$prefix" "$OTEL_HTTP_PORT" + printf '%sPOSTGRES_URL=%s\n' "$prefix" "$POSTGRES_URL" + printf '%sDATABASE_URL=%s\n' "$prefix" "$DATABASE_URL" + printf '%sREDIS_URL=%s\n' "$prefix" "$REDIS_URL" + printf '%sWEB_API_BASE_URL=%s\n' "$prefix" "$WEB_API_BASE_URL" + printf '%sOTEL_EXPORTER_OTLP_ENDPOINT=%s\n' "$prefix" "$OTEL_EXPORTER_OTLP_ENDPOINT" +} + +export_env() { + calculate_ports + export API_PORT WEB_PORT WORKER_HEALTH_PORT + export POSTGRES_HOST_PORT REDIS_HOST_PORT + export OTEL_HTTP_PORT POSTGRES_URL DATABASE_URL REDIS_URL + export WEB_API_BASE_URL OTEL_EXPORTER_OTLP_ENDPOINT +} + +run_with_env() { + if [ "$#" -eq 0 ]; then + usage + return 2 + fi + + # Allow one-off overrides before "--", matching the Python helper: + # ./scripts/worktree-ports.sh exec API_PORT=9000 -- ./scripts/dev.sh + overrides="" + while [ "$#" -gt 0 ]; do + case "$1" in + --) + shift + break + ;; + *=*) + export "$1" + overrides="${overrides} +$1" + shift + ;; + *) + usage + return 2 + ;; + esac + done + + if [ "$#" -eq 0 ]; then + usage + return 2 + fi + + export_env + while IFS= read -r assignment; do + if [ -n "$assignment" ]; then + export "$assignment" + fi + done < FastAPI: @app.get("/health", response_model=HealthResponse) def health() -> HealthResponse: + # Keep this endpoint dependency-free. Add separate readiness checks when + # a target service has real database or queue dependencies. return HealthResponse(service=settings.otel_service_name, status="ok") return app diff --git a/apps/api/tests/test_health.py b/stacks/python/apps/api/tests/test_health.py similarity index 100% rename from apps/api/tests/test_health.py rename to stacks/python/apps/api/tests/test_health.py diff --git a/apps/api/tests/test_postgres_integration.py b/stacks/python/apps/api/tests/test_postgres_integration.py similarity index 100% rename from apps/api/tests/test_postgres_integration.py rename to stacks/python/apps/api/tests/test_postgres_integration.py diff --git a/packages/shared/pyproject.toml b/stacks/python/packages/shared/pyproject.toml similarity index 100% rename from packages/shared/pyproject.toml rename to stacks/python/packages/shared/pyproject.toml diff --git a/packages/shared/src/example_shared/__init__.py b/stacks/python/packages/shared/src/example_shared/__init__.py similarity index 100% rename from packages/shared/src/example_shared/__init__.py rename to stacks/python/packages/shared/src/example_shared/__init__.py diff --git a/packages/shared/src/example_shared/observability.py b/stacks/python/packages/shared/src/example_shared/observability.py similarity index 100% rename from packages/shared/src/example_shared/observability.py rename to stacks/python/packages/shared/src/example_shared/observability.py diff --git a/packages/shared/src/example_shared/schemas.py b/stacks/python/packages/shared/src/example_shared/schemas.py similarity index 100% rename from packages/shared/src/example_shared/schemas.py rename to stacks/python/packages/shared/src/example_shared/schemas.py diff --git a/packages/shared/src/example_shared/settings.py b/stacks/python/packages/shared/src/example_shared/settings.py similarity index 74% rename from packages/shared/src/example_shared/settings.py rename to stacks/python/packages/shared/src/example_shared/settings.py index f31ae22..23b7afe 100644 --- a/packages/shared/src/example_shared/settings.py +++ b/stacks/python/packages/shared/src/example_shared/settings.py @@ -7,6 +7,8 @@ class Settings(BaseSettings): + # Keep boundary configuration centralized here. Stack consumers should add + # new env vars to this model and .env.example together. model_config = SettingsConfigDict( env_file=".env", env_file_encoding="utf-8", @@ -20,6 +22,8 @@ class Settings(BaseSettings): api_port: int = 8720 api_shared_secret: SecretStr | None = None + # Keep both names: application code often reads POSTGRES_URL, while tools + # such as Drizzle and migration scripts conventionally read DATABASE_URL. postgres_url: str = "postgresql://app:app@127.0.0.1:8740/app" database_url: str = "postgresql://app:app@127.0.0.1:8740/app" redis_url: str = "redis://127.0.0.1:8750/0" @@ -36,4 +40,5 @@ class Settings(BaseSettings): @lru_cache(maxsize=1) def get_settings() -> Settings: + # Cache settings so app startup and request handlers share one parsed model. return Settings() diff --git a/pyproject.toml b/stacks/python/pyproject.toml similarity index 96% rename from pyproject.toml rename to stacks/python/pyproject.toml index fb4c65a..1e44f98 100644 --- a/pyproject.toml +++ b/stacks/python/pyproject.toml @@ -10,10 +10,6 @@ dependencies = [ [tool.uv] package = false -exclude-newer = "7 days" - -[tool.uv.pip] -exclude-newer = "7 days" [tool.uv.sources] example-api = { workspace = true } diff --git a/stacks/python/scripts/check-all.sh b/stacks/python/scripts/check-all.sh new file mode 100755 index 0000000..d22f9d9 --- /dev/null +++ b/stacks/python/scripts/check-all.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env sh +set -eu + +cd "$(dirname "$0")/.." +export UV_LOCKED=1 + +./scripts/lint.sh +uv run ruff format --check apps packages tests +./scripts/typecheck.sh +./scripts/test.sh diff --git a/stacks/python/scripts/dev.sh b/stacks/python/scripts/dev.sh new file mode 100755 index 0000000..ba5ea89 --- /dev/null +++ b/stacks/python/scripts/dev.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env sh +set -eu + +cd "$(dirname "$0")/.." + +eval "$(./scripts/worktree-ports.py export)" +export API_HOST="${API_HOST:-127.0.0.1}" +export PYTHONPATH="${PYTHONPATH:-apps/api/src:packages/shared/src}" + +# This script is intentionally stack-local. Copy it to root scripts only after +# selecting the Python stack for a target repository. +echo "508 Devkit Python stack" +echo " API: http://${API_HOST}:${API_PORT}" +echo " Postgres: 127.0.0.1:${POSTGRES_HOST_PORT}" +echo " Redis: 127.0.0.1:${REDIS_HOST_PORT}" +echo + +uv run --package example-api uvicorn example_api.main:create_app \ + --factory \ + --host "$API_HOST" \ + --port "$API_PORT" \ + --reload \ + --reload-dir apps/api/src \ + --reload-dir packages/shared/src diff --git a/stacks/python/scripts/format.sh b/stacks/python/scripts/format.sh new file mode 100755 index 0000000..6ab79c5 --- /dev/null +++ b/stacks/python/scripts/format.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env sh +set -eu + +cd "$(dirname "$0")/.." +export UV_LOCKED=1 + +uv run ruff format apps packages tests diff --git a/stacks/python/scripts/lint.sh b/stacks/python/scripts/lint.sh new file mode 100755 index 0000000..008fb77 --- /dev/null +++ b/stacks/python/scripts/lint.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env sh +set -eu + +cd "$(dirname "$0")/.." +export UV_LOCKED=1 + +uv run ruff check apps packages tests diff --git a/stacks/python/scripts/test.sh b/stacks/python/scripts/test.sh new file mode 100755 index 0000000..921d9d1 --- /dev/null +++ b/stacks/python/scripts/test.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env sh +set -eu + +cd "$(dirname "$0")/.." +export UV_LOCKED=1 + +uv run pytest diff --git a/stacks/python/scripts/typecheck.sh b/stacks/python/scripts/typecheck.sh new file mode 100755 index 0000000..ae9a502 --- /dev/null +++ b/stacks/python/scripts/typecheck.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env sh +set -eu + +cd "$(dirname "$0")/.." +export UV_LOCKED=1 + +uv run mypy diff --git a/scripts/worktree-ports.py b/stacks/python/scripts/worktree-ports.py similarity index 51% rename from scripts/worktree-ports.py rename to stacks/python/scripts/worktree-ports.py index e674c36..9d2ccb9 100755 --- a/scripts/worktree-ports.py +++ b/stacks/python/scripts/worktree-ports.py @@ -13,16 +13,23 @@ BASE_PORT = 8700 SPAN = 1000 PORT_BLOCK_SIZE = 100 -OFFSETS = { +RESERVED_BLOCK_SIZE_DEFAULT = 10 +HASH_OFFSETS = { "API_PORT": 20, "WEB_PORT": 30, "WORKER_HEALTH_PORT": 35, "POSTGRES_HOST_PORT": 40, "REDIS_HOST_PORT": 50, - "MINIO_API_HOST_PORT": 60, - "MINIO_CONSOLE_HOST_PORT": 61, "OTEL_HTTP_PORT": 80, } +RESERVED_BLOCK_OFFSETS = { + "WEB_PORT": 0, + "API_PORT": 1, + "WORKER_HEALTH_PORT": 2, + "POSTGRES_HOST_PORT": 3, + "REDIS_HOST_PORT": 4, + "OTEL_HTTP_PORT": 5, +} WEB_RESTRICTED_PORTS = frozenset( { 1, @@ -110,6 +117,7 @@ def worktree_root() -> Path: + """Return the git worktree root, falling back to cwd outside git repos.""" try: output = subprocess.check_output( ["git", "rev-parse", "--show-toplevel"], @@ -122,6 +130,7 @@ def worktree_root() -> Path: def port_block(root: Path) -> int: + """Map a worktree path into a deterministic block of local ports.""" digest = hashlib.sha256(str(root.resolve()).encode("utf-8")).hexdigest() return BASE_PORT + ((int(digest[:8], 16) % (SPAN // PORT_BLOCK_SIZE)) * PORT_BLOCK_SIZE) @@ -132,31 +141,105 @@ def chrome_safe_port(port: int) -> int: return port -def ports_for_base(base: int) -> dict[str, int]: - values = {name: base + offset for name, offset in OFFSETS.items()} +def unused_port_in_block(start: int, end: int, used: set[int], *, browser_safe: bool = False) -> int: + port = start + while port <= end: + blocked = browser_safe and port in WEB_RESTRICTED_PORTS + if not blocked and port not in used: + return port + port += 1 + kind = "browser-safe free" if browser_safe else "free" + msg = f"reserved port block does not have enough {kind} ports" + raise ValueError(msg) + + +def ports_for_base(base: int, offsets: dict[str, int] | None = None) -> dict[str, int]: + """Apply offsets and sanitize browser-facing service ports.""" + selected_offsets = offsets or HASH_OFFSETS + values = {name: base + offset for name, offset in selected_offsets.items()} values["WEB_PORT"] = chrome_safe_port(values["WEB_PORT"]) values["API_PORT"] = chrome_safe_port(values["API_PORT"]) return values -def env_values() -> dict[str, str]: - values = ports_for_base(port_block(worktree_root())) +def ports_for_reserved_block(start: int, size: int) -> dict[str, int]: + """Allocate compact, unique ports inside a reserved block.""" + if size < len(RESERVED_BLOCK_OFFSETS): + msg = "WORKTREE_PORT_BLOCK_SIZE must be at least 6" + raise ValueError(msg) + + end = start + size - 1 + used: set[int] = set() + values: dict[str, int] = {} + for name, offset in RESERVED_BLOCK_OFFSETS.items(): + values[name] = unused_port_in_block( + start + offset, + end, + used, + browser_safe=name in {"API_PORT", "WEB_PORT"}, + ) + used.add(values[name]) + return values + + +def validate_unique_ports(values: dict[str, int]) -> None: + """Catch primary-port overrides that collide with generated service ports.""" + seen: set[int] = set() + for port in values.values(): + if port in seen: + msg = ( + f"worktree port reservation produced duplicate port {port}; " + "adjust WORKTREE_PRIMARY_PORT, WORKTREE_PRIMARY_PORT_TARGET, or WORKTREE_PORT_BLOCK_*" + ) + raise ValueError(msg) + seen.add(port) + + +def positive_int(value: str | None) -> int | None: + if value is None or not value.isdecimal(): + return None + parsed = int(value) + return parsed if parsed > 0 else None + + +def ports_for_environment(env: dict[str, str], root: Path | None = None) -> dict[str, int]: + """Use generic orchestrator reservations when present, otherwise hash the worktree.""" + block_start = positive_int(env.get("WORKTREE_PORT_BLOCK_START")) + if block_start is not None: + block_size = positive_int(env.get("WORKTREE_PORT_BLOCK_SIZE")) or RESERVED_BLOCK_SIZE_DEFAULT + values = ports_for_reserved_block(block_start, block_size) + else: + values = ports_for_base(port_block(root or worktree_root())) + + primary = positive_int(env.get("WORKTREE_PRIMARY_PORT")) + if primary is not None: + target = env.get("WORKTREE_PRIMARY_PORT_TARGET", "WEB_PORT") + if target not in {"API_PORT", "WEB_PORT"}: + msg = "WORKTREE_PRIMARY_PORT_TARGET must be API_PORT or WEB_PORT" + raise ValueError(msg) + values[target] = chrome_safe_port(primary) + + validate_unique_ports(values) + return values + + +def env_values(env: dict[str, str] | None = None) -> dict[str, str]: + """Return shell-friendly strings consumed by Compose and dev scripts.""" + values = ports_for_environment(env or os.environ) postgres = values["POSTGRES_HOST_PORT"] redis = values["REDIS_HOST_PORT"] api = values["API_PORT"] - minio = values["MINIO_API_HOST_PORT"] - env = {name: str(port) for name, port in values.items()} - env.update( + result = {name: str(port) for name, port in values.items()} + result.update( { "POSTGRES_URL": f"postgresql://app:app@127.0.0.1:{postgres}/app", "DATABASE_URL": f"postgresql://app:app@127.0.0.1:{postgres}/app", "REDIS_URL": f"redis://127.0.0.1:{redis}/0", - "MINIO_ENDPOINT": f"http://127.0.0.1:{minio}", "WEB_API_BASE_URL": f"http://127.0.0.1:{api}", "OTEL_EXPORTER_OTLP_ENDPOINT": f"http://127.0.0.1:{values['OTEL_HTTP_PORT']}", } ) - return env + return result def print_env(export: bool = False) -> None: @@ -170,16 +253,14 @@ def run_with_env(args: list[str]) -> int: print("usage: worktree-ports.py exec [KEY=VALUE ...] -- [args...]", file=sys.stderr) return 2 - env = os.environ.copy() - env.update(env_values()) - index = 0 + overrides: dict[str, str] = {} while index < len(args): token = args[index] if "=" not in token or token.startswith("-"): break key, value = token.split("=", 1) - env[key] = value + overrides[key] = value index += 1 if index >= len(args) or args[index] != "--": @@ -191,6 +272,14 @@ def run_with_env(args: list[str]) -> int: print("usage: worktree-ports.py exec [KEY=VALUE ...] -- [args...]", file=sys.stderr) return 2 + # Apply overrides before generation so generic reservation inputs affect the + # computed ports, then apply them again so direct API_PORT=... style + # overrides still win over generated values. + env = os.environ.copy() + env.update(overrides) + env.update(env_values(env)) + env.update(overrides) + return subprocess.run(command, env=env, check=False).returncode diff --git a/stacks/python/tests/test_worktree_ports.py b/stacks/python/tests/test_worktree_ports.py new file mode 100644 index 0000000..882671a --- /dev/null +++ b/stacks/python/tests/test_worktree_ports.py @@ -0,0 +1,94 @@ +from __future__ import annotations + +import importlib.util +from pathlib import Path + + +def load_worktree_ports_module(): + path = Path(__file__).resolve().parents[1] / "scripts" / "worktree-ports.py" + spec = importlib.util.spec_from_file_location("worktree_ports", path) + assert spec is not None + module = importlib.util.module_from_spec(spec) + assert spec.loader is not None + spec.loader.exec_module(module) + return module + + +def test_port_block_is_stable_for_same_path() -> None: + module = load_worktree_ports_module() + root = Path("/tmp/example-worktree") + + assert module.port_block(root) == module.port_block(root) + + +def test_ports_for_base_uses_expected_offsets() -> None: + module = load_worktree_ports_module() + + ports = module.ports_for_base(8700) + + assert ports["API_PORT"] == 8720 + assert ports["WEB_PORT"] == 8730 + assert ports["POSTGRES_HOST_PORT"] == 8740 + assert ports["REDIS_HOST_PORT"] == 8750 + assert ports["OTEL_HTTP_PORT"] == 8780 + + +def test_reserved_block_uses_compact_offsets() -> None: + module = load_worktree_ports_module() + + ports = module.ports_for_environment({"WORKTREE_PORT_BLOCK_START": "9000"}) + + assert ports["WEB_PORT"] == 9000 + assert ports["API_PORT"] == 9001 + assert ports["WORKER_HEALTH_PORT"] == 9002 + assert ports["POSTGRES_HOST_PORT"] == 9003 + assert ports["REDIS_HOST_PORT"] == 9004 + assert ports["OTEL_HTTP_PORT"] == 9005 + + +def test_reserved_block_skips_restricted_ports_without_collisions() -> None: + module = load_worktree_ports_module() + + ports = module.ports_for_environment({"WORKTREE_PORT_BLOCK_START": "6000"}) + + assert ports["WEB_PORT"] == 6001 + assert ports["API_PORT"] == 6002 + assert len(set(ports.values())) == len(ports) + + +def test_primary_port_can_target_api() -> None: + module = load_worktree_ports_module() + + ports = module.ports_for_environment( + { + "WORKTREE_PRIMARY_PORT": "9100", + "WORKTREE_PRIMARY_PORT_TARGET": "API_PORT", + }, + root=Path("/tmp/example-worktree"), + ) + + assert ports["API_PORT"] == 9100 + + +def test_primary_port_collision_raises_clear_error() -> None: + module = load_worktree_ports_module() + + try: + module.ports_for_environment( + { + "WORKTREE_PORT_BLOCK_START": "9000", + "WORKTREE_PRIMARY_PORT": "9000", + "WORKTREE_PRIMARY_PORT_TARGET": "API_PORT", + } + ) + except ValueError as error: + assert "duplicate port 9000" in str(error) + else: + raise AssertionError("expected duplicate primary port to fail") + + +def test_chrome_safe_port_skips_restricted_ports() -> None: + module = load_worktree_ports_module() + + assert module.chrome_safe_port(6000) == 6001 + assert module.chrome_safe_port(8730) == 8730 diff --git a/uv.lock b/stacks/python/uv.lock similarity index 99% rename from uv.lock rename to stacks/python/uv.lock index 0ebff4e..3956ae7 100644 --- a/uv.lock +++ b/stacks/python/uv.lock @@ -6,10 +6,6 @@ resolution-markers = [ "python_full_version < '3.15'", ] -[options] -exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for backwards compatibility when using relative exclude-newer values. -exclude-newer-span = "P7D" - [manifest] members = [ "devkit-python-workspace", diff --git a/stacks/typescript/README.md b/stacks/typescript/README.md new file mode 100644 index 0000000..450ca80 --- /dev/null +++ b/stacks/typescript/README.md @@ -0,0 +1,28 @@ +# TypeScript Stack + +Use this stack when the target repository needs TypeScript-side conventions, +whether or not it has a browser-facing app. + +This stack is framework-neutral. It captures package scripts, TypeScript, +Biome, Vitest, Drizzle examples, and environment naming. It is not a Vite, +Next.js, Astro, TanStack Start, or Expo scaffold. + +## Agent Notes + +- Inspect the target repo before copying this stack. +- Keep the stack if the repo needs TypeScript tooling, shared TypeScript + contracts, Drizzle schema examples, or frontend conventions. +- Do not infer a frontend framework from this stack. Choose a framework only + after the product shape and deployment target are clear. +- Replace placeholder tables, package names, and env values with target-specific + contracts. +- Run `bun run lint`, `bun run typecheck`, `bun run test`, and `bun run build` + after adapting the stack. + +## Files + +- `package.json`: stack-local scripts and dependencies. +- `src/index.ts`: neutral API base URL helper. +- `src/db/schema.ts`: placeholder Drizzle schema example. +- `tests/`: Vitest example coverage. +- `pnpm/`: package-manager stack variant for larger TypeScript workspaces. diff --git a/apps/web/drizzle.config.ts b/stacks/typescript/drizzle.config.ts similarity index 100% rename from apps/web/drizzle.config.ts rename to stacks/typescript/drizzle.config.ts diff --git a/apps/web/package.json b/stacks/typescript/package.json similarity index 100% rename from apps/web/package.json rename to stacks/typescript/package.json diff --git a/stacks/typescript/pnpm/README.md b/stacks/typescript/pnpm/README.md new file mode 100644 index 0000000..ccd131a --- /dev/null +++ b/stacks/typescript/pnpm/README.md @@ -0,0 +1,40 @@ +# pnpm Stack Variant + +The root TypeScript example shows Bun first. Use these files when a project +prefers pnpm, usually for a larger JavaScript workspace or a team that already +standardizes on pnpm. + +Copy these files over the root equivalents: + +- `package.json` +- `pnpm-workspace.yaml` + +Then generate and commit the pnpm lockfile before enabling frozen installs: + +```bash +pnpm install +git add pnpm-lock.yaml +``` + +Then update CI install commands from: + +```bash +bun install --frozen-lockfile +``` + +to: + +```bash +pnpm install --frozen-lockfile +``` + +Keep `bunfig.toml` out of pnpm projects unless Bun is still used for local scripts. + +## Agent Notes + +- Use this variant only when pnpm is already preferred or the TypeScript + workspace needs pnpm's monorepo behavior. +- Update CI install commands and package scripts together. +- Do not enable `pnpm install --frozen-lockfile` until `pnpm-lock.yaml` is + generated and committed. +- Keep `minimumReleaseAge: 10080` in `pnpm-workspace.yaml`. diff --git a/alternates/pnpm/ci-web-job.yml b/stacks/typescript/pnpm/ci-web-job.yml similarity index 62% rename from alternates/pnpm/ci-web-job.yml rename to stacks/typescript/pnpm/ci-web-job.yml index 0673321..2fc802a 100644 --- a/alternates/pnpm/ci-web-job.yml +++ b/stacks/typescript/pnpm/ci-web-job.yml @@ -1,8 +1,10 @@ -web: +typescript: needs: changes - if: needs.changes.outputs.web == 'true' || needs.changes.outputs.workflow == 'true' + if: needs.changes.outputs.typescript == 'true' || needs.changes.outputs.workflow == 'true' runs-on: ubuntu-latest steps: + # This fragment assumes the pnpm variant has generated and committed + # pnpm-lock.yaml. Frozen installs fail by design when the lockfile is absent. - uses: actions/checkout@v5 - uses: pnpm/action-setup@v4 @@ -18,7 +20,7 @@ web: run: pnpm install --frozen-lockfile - name: Biome - run: pnpm run format:check && pnpm run lint + run: pnpm run lint - name: Typecheck run: pnpm run typecheck diff --git a/stacks/typescript/pnpm/package.json b/stacks/typescript/pnpm/package.json new file mode 100644 index 0000000..ba85923 --- /dev/null +++ b/stacks/typescript/pnpm/package.json @@ -0,0 +1,27 @@ +{ + "name": "508-devkit", + "version": "0.1.0", + "private": true, + "type": "module", + "packageManager": "pnpm@10.33.0", + "scripts": { + "dev": "./scripts/dev.sh", + "build": "pnpm -C stacks/typescript run build", + "lint": "biome check .", + "format": "biome format --write .", + "format:check": "biome format .", + "typecheck": "pnpm -C stacks/typescript run typecheck", + "test": "pnpm -r run test", + "check": "pnpm run lint && pnpm run typecheck && pnpm run test && pnpm run build", + "ports": "./scripts/worktree-ports.sh env", + "db:generate": "pnpm -C stacks/typescript run db:generate", + "db:push": "pnpm -C stacks/typescript run db:push", + "db:studio": "pnpm -C stacks/typescript run db:studio" + }, + "devDependencies": { + "@biomejs/biome": "^2.4.13", + "@types/bun": "^1.3.4", + "@types/node": "^22.15.29", + "typescript": "^5.9.3" + } +} diff --git a/alternates/pnpm/pnpm-workspace.yaml b/stacks/typescript/pnpm/pnpm-workspace.yaml similarity index 87% rename from alternates/pnpm/pnpm-workspace.yaml rename to stacks/typescript/pnpm/pnpm-workspace.yaml index 5f5a4ef..8a56e90 100644 --- a/alternates/pnpm/pnpm-workspace.yaml +++ b/stacks/typescript/pnpm/pnpm-workspace.yaml @@ -1,5 +1,5 @@ packages: - - "apps/web" + - "stacks/typescript" # Seven days, in minutes. Keep this in pnpm-workspace.yaml for pnpm v10.16+. minimumReleaseAge: 10080 diff --git a/apps/web/src/db/schema.ts b/stacks/typescript/src/db/schema.ts similarity index 64% rename from apps/web/src/db/schema.ts rename to stacks/typescript/src/db/schema.ts index 4dac1dc..4c7cada 100644 --- a/apps/web/src/db/schema.ts +++ b/stacks/typescript/src/db/schema.ts @@ -1,5 +1,7 @@ import { pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core"; +// This is a placeholder contract, not product data. Replace it with the target +// repo's real tables when selecting Drizzle for TypeScript-side database access. export const exampleRecords = pgTable("example_records", { id: uuid("id").primaryKey().defaultRandom(), name: text("name").notNull(), diff --git a/stacks/typescript/src/index.ts b/stacks/typescript/src/index.ts new file mode 100644 index 0000000..59f3ba6 --- /dev/null +++ b/stacks/typescript/src/index.ts @@ -0,0 +1,5 @@ +export function apiBaseUrl(env: Record = {}): string { + // Use a neutral env name in the stack. Map it to NEXT_PUBLIC_*, VITE_*, or + // another framework-specific public variable only after choosing a framework. + return env.WEB_API_BASE_URL ?? "http://127.0.0.1:8720"; +} diff --git a/apps/web/tests/index.test.ts b/stacks/typescript/tests/index.test.ts similarity index 100% rename from apps/web/tests/index.test.ts rename to stacks/typescript/tests/index.test.ts diff --git a/apps/web/tsconfig.json b/stacks/typescript/tsconfig.json similarity index 100% rename from apps/web/tsconfig.json rename to stacks/typescript/tsconfig.json diff --git a/apps/web/vitest.config.ts b/stacks/typescript/vitest.config.ts similarity index 100% rename from apps/web/vitest.config.ts rename to stacks/typescript/vitest.config.ts diff --git a/tests/test_worktree_ports.py b/tests/test_worktree_ports.py deleted file mode 100644 index 672ffbb..0000000 --- a/tests/test_worktree_ports.py +++ /dev/null @@ -1,41 +0,0 @@ -from __future__ import annotations - -import importlib.util -from pathlib import Path - - -def load_worktree_ports_module(): - path = Path(__file__).resolve().parents[1] / "scripts" / "worktree-ports.py" - spec = importlib.util.spec_from_file_location("worktree_ports", path) - assert spec is not None - module = importlib.util.module_from_spec(spec) - assert spec.loader is not None - spec.loader.exec_module(module) - return module - - -def test_port_block_is_stable_for_same_path() -> None: - module = load_worktree_ports_module() - root = Path("/tmp/example-worktree") - - assert module.port_block(root) == module.port_block(root) - - -def test_ports_for_base_uses_expected_offsets() -> None: - module = load_worktree_ports_module() - - ports = module.ports_for_base(8700) - - assert ports["API_PORT"] == 8720 - assert ports["WEB_PORT"] == 8730 - assert ports["POSTGRES_HOST_PORT"] == 8740 - assert ports["REDIS_HOST_PORT"] == 8750 - assert ports["MINIO_API_HOST_PORT"] == 8760 - assert ports["OTEL_HTTP_PORT"] == 8780 - - -def test_chrome_safe_port_skips_restricted_ports() -> None: - module = load_worktree_ports_module() - - assert module.chrome_safe_port(6000) == 6001 - assert module.chrome_safe_port(8730) == 8730