Skip to content
Closed
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
e26c708
feat: add system-aware tiered parallel pytest + cgroup v1/v2 support …
LahkLeKey Jun 6, 2026
4fbaa2c
feat: add system-aware tiered parallel pytest + cgroup v1/v2 support …
LahkLeKey Jun 6, 2026
127ac67
test: harden upstream review fixes and unify path helpers
LahkLeKey Jun 6, 2026
a227754
test: address latest upstream review findings
LahkLeKey Jun 6, 2026
42571cc
fix tests path normalization and xdist defaults
LahkLeKey Jun 6, 2026
b12061f
fix review comments on parallel and path tests
LahkLeKey Jun 6, 2026
b87e7c6
fix latest review comments and harden tests
LahkLeKey Jun 6, 2026
8bb0ca4
fix latest review comments on path and xdist parsing
LahkLeKey Jun 6, 2026
55819c7
fix latest review comments on installer and PATH tests
LahkLeKey Jun 6, 2026
4c2a128
fix latest posix-vs-git-bash review comments
LahkLeKey Jun 6, 2026
1ce7a90
fix latest review comments and tighten guidance
LahkLeKey Jun 6, 2026
1c95074
fix latest review comments and reduce test churn
LahkLeKey Jun 6, 2026
ba72907
test latest review edge cases for bash and parallel hooks
LahkLeKey Jun 6, 2026
d7ba3f6
fix latest review comments on bash and parallel validation
LahkLeKey Jun 7, 2026
f78887f
fix latest review comments on path and parallel hooks
LahkLeKey Jun 7, 2026
0bb271f
fix latest review comments and consolidate bash helpers
LahkLeKey Jun 7, 2026
8e07002
address remaining review comments
LahkLeKey Jun 7, 2026
98b56f0
update PR review comment guidance
LahkLeKey Jun 7, 2026
822e9f8
ignore pr scratch files
LahkLeKey Jun 7, 2026
e00e647
preserve preset scan semantics
LahkLeKey Jun 7, 2026
71e9e04
address latest review feedback
LahkLeKey Jun 7, 2026
100b994
harden preset scan fallback pipeline
LahkLeKey Jun 7, 2026
9d3ddbc
harden resolver and windows chmod tests
LahkLeKey Jun 7, 2026
1c10388
tighten review comments on xdist and auth tests
LahkLeKey Jun 7, 2026
afd927d
stabilize interpreter selection and review fixes
LahkLeKey Jun 7, 2026
005a5da
test: address latest review hardening comments
LahkLeKey Jun 7, 2026
8e922e0
test: fix latest review feedback round
LahkLeKey Jun 7, 2026
7af875d
test: address latest review comments
LahkLeKey Jun 7, 2026
0b201ac
test: resolve latest review follow-ups
LahkLeKey Jun 7, 2026
7727673
test: harden latest review edge cases
LahkLeKey Jun 7, 2026
1858a93
test: prevent preset strategy state leakage
LahkLeKey Jun 7, 2026
45d0fbd
test: resolve latest copilot review comments
LahkLeKey Jun 7, 2026
2b5fa8e
test: consolidate bash resolver test helpers
LahkLeKey Jun 7, 2026
015b976
test: refine parallel arg injection and probe reuse
LahkLeKey Jun 8, 2026
093b04b
fix: harden manifest path traversal guard
LahkLeKey Jun 8, 2026
78d7988
fix: harden parser and path edge cases
LahkLeKey Jun 8, 2026
da4953a
fix: tighten manifest and device path guards
LahkLeKey Jun 8, 2026
8ec70b0
merge: continue upstream-pr2877 work
LahkLeKey Jun 8, 2026
ab43b6d
chore: start fork continuation PR3
LahkLeKey Jun 8, 2026
c3e7b75
test: address PR3 copilot follow-ups
LahkLeKey Jun 8, 2026
c926913
test: remove stale self-upgrade import
LahkLeKey Jun 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<!-- How did you test your changes? -->

- [ ] Tested locally with `uv run specify --help`
- [ ] Ran existing tests with `uv sync && uv run pytest`
- [ ] Ran existing tests with `uv sync && uv run pytest` (optionally `uv run pytest --parallel --parallel-tier medium`)
Comment thread
LahkLeKey marked this conversation as resolved.
- [ ] Tested with a sample project (if applicable)

## AI Disclosure
Expand All @@ -17,6 +17,7 @@

- [ ] I **did not** use AI assistance for this contribution
- [ ] I **did** use AI assistance (describe below)
- [ ] If AI posted PR comments on my behalf, each comment includes explicit "Posted on behalf of @<me> by <agent> (model: <model>)" attribution
Comment thread
LahkLeKey marked this conversation as resolved.

<!-- If you used AI, briefly describe how (e.g., "Code generated by Copilot", "Consulted ChatGPT for approach"): -->

1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ env/
*.swo
.DS_Store
*.tmp
.tmp_*

# Project specific
*.log
Expand Down
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -426,10 +426,12 @@ When an issue exists, include its number immediately after the prefix — this i
## Responding to PR Review Comments

- If you are an agent working on behalf of a human, **disclose your identity in your PR comment** — name the agent (and model, if applicable) and the human you are acting for (e.g., "Posted on behalf of @user by GitHub Copilot (model: &lt;name-if-known&gt;)").
- When an AI-generated PR comment is posted on your behalf, include an explicit attribution line in the comment body before the substantive update, such as "Posted on behalf of @user by GitHub Copilot (model: GPT-5.4 mini).".
- Post **one** top-level summary comment per review round listing what changed and the commit SHA. Do not reply on every individual comment.
- Reply inline only when context is needed (disagreement, deferral, non-obvious fix). Keep it to a sentence or two.
- **Never click "Resolve conversation"** — that belongs to the reviewer or PR author.
- No emoji, no celebratory framing, no checklist mirroring the reviewer's items, no restating what the reviewer wrote.
- Keep the PR description Review Fix Ledger additive with commit-level entries for each review round, and refresh the Copilot guidance when new evidence shows a feedback-loop risk.
- Re-request review once per round (when all feedback is addressed), not after every intermediate push.

---
Expand Down
33 changes: 33 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ On [GitHub Codespaces](https://github.com/features/codespaces) it's even simpler
1. Fork and clone the repository
1. Configure and install the dependencies: `uv sync --extra test`
1. Make sure the CLI works on your machine: `uv run specify --help`
1. Run tests: `uv run pytest` (optional faster path: `uv run pytest --parallel`)
1. Create a new branch: `git checkout -b <type>/<number>-<short-slug>` (see [Branch naming](#branch-naming) below)
1. Make your change, add tests, and make sure everything still works
1. Test the CLI functionality with a sample project if relevant
Expand Down Expand Up @@ -87,6 +88,32 @@ For the smoothest review experience, validate changes in this order:

### Automated checks

#### Optional parallel test execution

```bash
uv run pytest --parallel
```

`--parallel` is opt-in and auto-selects a conservative worker count using CPU, memory, and OS caps. Use `--parallel-max-workers N` to set a stricter upper bound.

Worker settings are calculated from effective CPU capacity (including affinity/container quotas where available) and currently available memory, then bounded by platform caps.

Use `--parallel-tier low|medium|high` to tune aggressiveness:

- `low` keeps more headroom (best for laptops or multitasking)
- `medium` is the default balance
- `high` favors throughput on dedicated dev/CI machines

Recommended starting points:

| Environment | Suggested tier | Example command |
| --- | --- | --- |
| Laptop / shared desktop | low | `uv run pytest --parallel --parallel-tier low` |
| Developer workstation | medium | `uv run pytest --parallel --parallel-tier medium` |
| Dedicated CI runner | high | `uv run pytest --parallel --parallel-tier high` |
Comment thread
LahkLeKey marked this conversation as resolved.

If system load is high or tests become unstable, step down one tier and/or set `--parallel-max-workers`.

#### Agent configuration and wiring consistency

```bash
Expand Down Expand Up @@ -190,6 +217,12 @@ That being said, if you are using any kind of AI assistance (e.g., agents, ChatG

If your PR responses or comments are being generated by an AI, disclose that as well.

When AI-generated PR comments are posted on your behalf, use an explicit attribution line in the comment body, for example:

> Posted on behalf of @<your-handle> by GitHub Copilot (model: GPT-5.3-Codex).

Keep one top-level review-round summary comment per round (instead of replying to every thread), and do not resolve reviewer conversations yourself.

As an exception, trivial spacing or typo fixes don't need to be disclosed, so long as the changes are limited to small parts of the code or short phrases.

An example disclosure:
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ packages = ["src/specify_cli"]
test = [
"pytest>=7.0",
"pytest-cov>=4.0",
"pytest-xdist>=3.6.1",
]

[tool.pytest.ini_options]
Expand Down
256 changes: 136 additions & 120 deletions scripts/bash/common.sh
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,83 @@ json_escape() {
check_file() { [[ -f "$1" ]] && echo " ✓ $2" || echo " ✗ $2"; }
check_dir() { [[ -d "$1" && -n $(ls -A "$1" 2>/dev/null) ]] && echo " ✓ $2" || echo " ✗ $2"; }

_RESOLVE_TEMPLATE_PYTHON_CMD=""

_is_python3_command() {
local cmd="$1"
command -v "$cmd" >/dev/null 2>&1 || return 1
"$cmd" -c 'import sys; sys.exit(0 if sys.version_info[0] >= 3 else 1)' >/dev/null 2>&1
}

resolve_template_python_cmd() {
if [ -n "$_RESOLVE_TEMPLATE_PYTHON_CMD" ]; then
return 0
fi
if _is_python3_command "python3"; then
_RESOLVE_TEMPLATE_PYTHON_CMD="python3"
return 0
fi
if _is_python3_command "python"; then
_RESOLVE_TEMPLATE_PYTHON_CMD="python"
return 0
fi
return 1
}
Comment thread
LahkLeKey marked this conversation as resolved.
Comment thread
LahkLeKey marked this conversation as resolved.
Comment thread
LahkLeKey marked this conversation as resolved.

_iter_preset_ids_ordered() {
local presets_dir="$1"
local registry_file="$presets_dir/.registry"
local python_cmd=""

if [ -f "$registry_file" ] && resolve_template_python_cmd; then
python_cmd="$_RESOLVE_TEMPLATE_PYTHON_CMD"
if SPECKIT_REGISTRY="$registry_file" "$python_cmd" -c "
Comment thread
LahkLeKey marked this conversation as resolved.
import json, sys, os
def priority_key(meta):
if not isinstance(meta, dict):
return 10
raw = meta.get('priority', 10)
try:
return int(raw)
except (TypeError, ValueError):
return 10
try:
with open(os.environ['SPECKIT_REGISTRY']) as f:
data = json.load(f)
presets = data.get('presets', {})
for pid, meta in sorted(
presets.items(),
key=lambda x: (
priority_key(x[1]),
x[0],
),
):
if isinstance(meta, dict) and meta.get('enabled', True) is not False:
print(pid)
except Exception:
sys.exit(1)
Comment thread
LahkLeKey marked this conversation as resolved.
" 2>/dev/null; then
Comment thread
LahkLeKey marked this conversation as resolved.
return 0
fi
fi

find "$presets_dir" -mindepth 1 -maxdepth 1 -type d ! -name '.*' 2>/dev/null \
| LC_ALL=C sort \
Comment thread
LahkLeKey marked this conversation as resolved.
| while IFS= read -r preset; do
[ -n "$preset" ] || continue
basename "$preset"
done
}

_is_safe_preset_id() {
local preset_id="$1"
[ -n "$preset_id" ] || return 1
case "$preset_id" in
.|..|.*|*/*|*\\*) return 1 ;;
esac
return 0
}
Comment thread
LahkLeKey marked this conversation as resolved.
Comment thread
LahkLeKey marked this conversation as resolved.

# Resolve a template name to a file path using the priority stack:
# 1. .specify/templates/overrides/
# 2. .specify/presets/<preset-id>/templates/ (sorted by priority from .registry)
Expand All @@ -433,47 +510,15 @@ resolve_template() {
local presets_dir="$repo_root/.specify/presets"
if [ -d "$presets_dir" ]; then
local registry_file="$presets_dir/.registry"
if [ -f "$registry_file" ] && command -v python3 >/dev/null 2>&1; then
# Read preset IDs sorted by priority (lower number = higher precedence).
# The python3 call is wrapped in an if-condition so that set -e does not
# abort the function when python3 exits non-zero (e.g. invalid JSON).
local sorted_presets=""
if sorted_presets=$(SPECKIT_REGISTRY="$registry_file" python3 -c "
import json, sys, os
try:
with open(os.environ['SPECKIT_REGISTRY']) as f:
data = json.load(f)
presets = data.get('presets', {})
for pid, meta in sorted(presets.items(), key=lambda x: x[1].get('priority', 10) if isinstance(x[1], dict) else 10):
if isinstance(meta, dict) and meta.get('enabled', True) is not False:
print(pid)
except Exception:
sys.exit(1)
" 2>/dev/null); then
if [ -n "$sorted_presets" ]; then
# python3 succeeded and returned preset IDs — search in priority order
while IFS= read -r preset_id; do
local candidate="$presets_dir/$preset_id/templates/${template_name}.md"
[ -f "$candidate" ] && echo "$candidate" && return 0
done <<< "$sorted_presets"
fi
# python3 succeeded but registry has no presets — nothing to search
else
# python3 failed (missing, or registry parse error) — fall back to unordered directory scan
for preset in "$presets_dir"/*/; do
[ -d "$preset" ] || continue
local candidate="$preset/templates/${template_name}.md"
[ -f "$candidate" ] && echo "$candidate" && return 0
done
fi
else
# Fallback: alphabetical directory order (no python3 available)
for preset in "$presets_dir"/*/; do
[ -d "$preset" ] || continue
local candidate="$preset/templates/${template_name}.md"
[ -f "$candidate" ] && echo "$candidate" && return 0
done
if [ -f "$registry_file" ]; then
resolve_template_python_cmd || true
fi
while IFS= read -r preset_id; do
preset_id="${preset_id%$'\r'}"
_is_safe_preset_id "$preset_id" || continue
local candidate="$presets_dir/$preset_id/templates/${template_name}.md"
[ -f "$candidate" ] && echo "$candidate" && return 0
done < <(_iter_preset_ids_ordered "$presets_dir")
Comment thread
LahkLeKey marked this conversation as resolved.
fi

# Priority 3: Extension-provided templates
Expand Down Expand Up @@ -524,33 +569,24 @@ resolve_template_content() {
local presets_dir="$repo_root/.specify/presets"
if [ -d "$presets_dir" ]; then
local registry_file="$presets_dir/.registry"
local sorted_presets=""
if [ -f "$registry_file" ] && command -v python3 >/dev/null 2>&1; then
if sorted_presets=$(SPECKIT_REGISTRY="$registry_file" python3 -c "
import json, sys, os
try:
with open(os.environ['SPECKIT_REGISTRY']) as f:
data = json.load(f)
presets = data.get('presets', {})
for pid, meta in sorted(presets.items(), key=lambda x: x[1].get('priority', 10) if isinstance(x[1], dict) else 10):
if isinstance(meta, dict) and meta.get('enabled', True) is not False:
print(pid)
except Exception:
sys.exit(1)
" 2>/dev/null); then
if [ -n "$sorted_presets" ]; then
local yaml_warned=false
while IFS= read -r preset_id; do
# Read strategy and file path from preset manifest
local strategy="replace"
local manifest_file=""
local manifest="$presets_dir/$preset_id/preset.yml"
if [ -f "$manifest" ] && command -v python3 >/dev/null 2>&1; then
# Requires PyYAML; falls back to replace/convention if unavailable
local result
local py_stderr
py_stderr=$(mktemp)
result=$(SPECKIT_MANIFEST="$manifest" SPECKIT_TMPL="$template_name" python3 -c "
if [ -f "$registry_file" ]; then
resolve_template_python_cmd || true
fi
local manifest_python_cmd="$_RESOLVE_TEMPLATE_PYTHON_CMD"
local yaml_warned=false
while IFS= read -r preset_id; do
preset_id="${preset_id%$'\r'}"
_is_safe_preset_id "$preset_id" || continue
# Read strategy and file path from preset manifest
local strategy="replace"
local manifest_file=""
local manifest="$presets_dir/$preset_id/preset.yml"
if [ -f "$manifest" ] && [ -n "$manifest_python_cmd" ]; then
# Requires PyYAML; falls back to replace/convention if unavailable
local result
local py_stderr
py_stderr=$(mktemp)
result=$(SPECKIT_MANIFEST="$manifest" SPECKIT_TMPL="$template_name" "$manifest_python_cmd" -c "
Comment thread
LahkLeKey marked this conversation as resolved.
Comment thread
LahkLeKey marked this conversation as resolved.
import sys, os
try:
import yaml
Expand All @@ -569,61 +605,41 @@ try:
except Exception:
print('replace\t')
" 2>"$py_stderr")
local parse_status=$?
if [ $parse_status -eq 0 ] && [ -n "$result" ]; then
IFS=$'\t' read -r strategy manifest_file <<< "$result"
strategy=$(printf '%s' "$strategy" | tr '[:upper:]' '[:lower:]')
fi
if [ "$yaml_warned" = false ] && grep -q 'yaml_missing' "$py_stderr" 2>/dev/null; then
echo "Warning: PyYAML not available; composition strategies may be ignored" >&2
yaml_warned=true
fi
rm -f "$py_stderr"
fi
# Try manifest file path first, then convention path
local candidate=""
if [ -n "$manifest_file" ]; then
# Reject absolute paths and parent traversal
case "$manifest_file" in
/*|*../*|../*) manifest_file="" ;;
esac
fi
if [ -n "$manifest_file" ]; then
local mf="$presets_dir/$preset_id/$manifest_file"
[ -f "$mf" ] && candidate="$mf"
fi
if [ -z "$candidate" ]; then
local cf="$presets_dir/$preset_id/templates/${template_name}.md"
[ -f "$cf" ] && candidate="$cf"
fi
if [ -n "$candidate" ]; then
layer_paths+=("$candidate")
layer_strategies+=("$strategy")
fi
done <<< "$sorted_presets"
local parse_status=$?
Comment thread
LahkLeKey marked this conversation as resolved.
if [ $parse_status -eq 0 ] && [ -n "$result" ]; then
IFS=$'\t' read -r strategy manifest_file <<< "$result"
strategy=$(printf '%s' "$strategy" | tr '[:upper:]' '[:lower:]')
else
strategy="replace"
manifest_file=""
Comment thread
LahkLeKey marked this conversation as resolved.
fi
Comment thread
LahkLeKey marked this conversation as resolved.
else
# python3 failed — fall back to unordered directory scan (replace only)
for preset in "$presets_dir"/*/; do
[ -d "$preset" ] || continue
local candidate="$preset/templates/${template_name}.md"
if [ -f "$candidate" ]; then
layer_paths+=("$candidate")
layer_strategies+=("replace")
fi
done
fi
else
# No python3 or registry — fall back to unordered directory scan (replace only)
for preset in "$presets_dir"/*/; do
[ -d "$preset" ] || continue
local candidate="$preset/templates/${template_name}.md"
if [ -f "$candidate" ]; then
layer_paths+=("$candidate")
layer_strategies+=("replace")
if [ "$yaml_warned" = false ] && grep -q 'yaml_missing' "$py_stderr" 2>/dev/null; then
echo "Warning: PyYAML not available; composition strategies may be ignored" >&2
yaml_warned=true
fi
done
fi
rm -f "$py_stderr"
fi
# Try manifest file path first, then convention path
local candidate=""
if [ -n "$manifest_file" ]; then
# Reject absolute paths and parent traversal
case "$manifest_file" in
/*|*../*|../*) manifest_file="" ;;
esac
fi
if [ -n "$manifest_file" ]; then
local mf="$presets_dir/$preset_id/$manifest_file"
[ -f "$mf" ] && candidate="$mf"
Comment thread
LahkLeKey marked this conversation as resolved.
fi
if [ -z "$candidate" ]; then
local cf="$presets_dir/$preset_id/templates/${template_name}.md"
[ -f "$cf" ] && candidate="$cf"
fi
if [ -n "$candidate" ]; then
layer_paths+=("$candidate")
layer_strategies+=("$strategy")
fi
done < <(_iter_preset_ids_ordered "$presets_dir")
fi

# Priority 3: Extension-provided templates (always "replace")
Expand Down
Loading