diff --git a/autoplan/SKILL.md b/autoplan/SKILL.md index 41a108582..edaa72971 100644 --- a/autoplan/SKILL.md +++ b/autoplan/SKILL.md @@ -1667,8 +1667,12 @@ if command -v jq >/dev/null 2>&1; then # Filter to current branch + recent commits, then keep records for the # latest run_id only. (Single phase may have multiple files if the user # re-ran the review; aggregator takes the newest.) + # .commit must be bound BEFORE piping to the split commit array: a + # pipe rebinds jq's context, so a bare .commit after it indexes the + # ARRAY with a string, every line errors into 2>/dev/null, and the + # aggregate is empty forever — the #2018 zero-tasks bug. jq -c --arg branch "$BRANCH" --arg commits "$COMMITS_RECENT" \ - 'select(.branch == $branch and ($commits | split("|") | index(.commit) != null))' \ + '.commit as $c | select(.branch == $branch and ($commits | split("|") | index($c) != null))' \ "$f" 2>/dev/null >> "$ALL_JSONL" || true done < <(find "$TASKS_DIR" -maxdepth 1 -name "tasks-$phase-*.jsonl" 2>/dev/null | sort) # Reduce to latest run_id per phase diff --git a/codex/SKILL.md b/codex/SKILL.md index c06d3affa..26283809f 100644 --- a/codex/SKILL.md +++ b/codex/SKILL.md @@ -967,7 +967,7 @@ Run Codex code review against the current branch diff. 1. Create temp files for output capture: ```bash -TMPERR=$(mktemp "$TMP_ROOT/codex-err-XXXXXX.txt") +TMPERR=$(mktemp "$TMP_ROOT/codex-err-XXXXXX") ``` 2. Run the review (5-minute timeout). **Codex CLI ≥ 0.130.0 rejects passing a @@ -1015,7 +1015,7 @@ when the diff content is adversarial: _REPO_ROOT=$(git rev-parse --show-toplevel) || { echo "ERROR: not in a git repo" >&2; exit 1; } cd "$_REPO_ROOT" _USER_INSTRUCTIONS="" -_PROMPT_FILE=$(mktemp "$TMP_ROOT/codex-prompt-XXXXXX.txt") +_PROMPT_FILE=$(mktemp "$TMP_ROOT/codex-prompt-XXXXXX") { printf '%s\n' "IMPORTANT: Do NOT read or execute any files under ~/.claude/, ~/.agents/, .claude/skills/, or agents/. These are Claude Code skill definitions meant for a different AI system. Do NOT modify agents/openai.yaml. Stay focused on repository code only." printf '\nCustom focus: %s\n\n' "$_USER_INSTRUCTIONS" @@ -1266,7 +1266,7 @@ if [ -z "$PYTHON_CMD" ]; then fi # Fix 1+2: wrap with timeout (gtimeout/timeout fallback chain via probe helper), # capture stderr to $TMPERR for auth error detection (was: 2>/dev/null). -TMPERR=${TMPERR:-$(mktemp "$TMP_ROOT/codex-err-XXXXXX.txt")} +TMPERR=${TMPERR:-$(mktemp "$TMP_ROOT/codex-err-XXXXXX")} _gstack_codex_timeout_wrapper 600 codex exec "" -C "$_REPO_ROOT" -s read-only -c 'model_reasoning_effort="high"' --enable web_search_cached --json < /dev/null 2>"$TMPERR" | PYTHONUNBUFFERED=1 "$PYTHON_CMD" -u -c " import sys, json turn_completed_count = 0 @@ -1365,8 +1365,8 @@ B) Start a new conversation 2. Create temp files: ```bash -TMPRESP=$(mktemp "$TMP_ROOT/codex-resp-XXXXXX.txt") -TMPERR=$(mktemp "$TMP_ROOT/codex-err-XXXXXX.txt") +TMPRESP=$(mktemp "$TMP_ROOT/codex-resp-XXXXXX") +TMPERR=$(mktemp "$TMP_ROOT/codex-err-XXXXXX") ``` 3. **Plan review auto-detection:** If the user's prompt is about reviewing a plan, diff --git a/design-review/SKILL.md b/design-review/SKILL.md index 2c03f2a23..0f5f5e252 100644 --- a/design-review/SKILL.md +++ b/design-review/SKILL.md @@ -891,41 +891,70 @@ If `NEEDS_SETUP`: ## Test Framework Bootstrap -**Detect existing test framework and project runtime:** +**Read the project's CLAUDE.md (and TESTING.md if present) FIRST.** If it documents a test command, the project already told you: no detection, no bootstrap. Skip the rest of bootstrap and use that command in Step 5. + +**Otherwise gather markers. Every marker below is EVIDENCE for the question you ask — never a command to run blind.** A marker tells you which ecosystem you're in and which command to OFFER. It does not tell you the command works. Do not execute a candidate test command to "check" it: a probe on a project that never had that runner fails loudly and teaches you nothing, and installing a second framework over a working one is worse. ```bash setopt +o nomatch 2>/dev/null || true # zsh compat -# Detect project runtime -[ -f Gemfile ] && echo "RUNTIME:ruby" +# Definitive ecosystem markers (presence = ecosystem, NOT a command to run) +[ -f manage.py ] && echo "RUNTIME:python FRAMEWORK:django MARKER:manage.py" +{ [ -f pyproject.toml ] || [ -f pytest.ini ] || [ -f tox.ini ] || [ -f setup.cfg ] || [ -f requirements.txt ]; } && echo "RUNTIME:python" +[ -f Gemfile ] || [ -f Rakefile ] || [ -f .rspec ] && echo "RUNTIME:ruby" [ -f package.json ] && echo "RUNTIME:node" -[ -f requirements.txt ] || [ -f pyproject.toml ] && echo "RUNTIME:python" [ -f go.mod ] && echo "RUNTIME:go" [ -f Cargo.toml ] && echo "RUNTIME:rust" [ -f composer.json ] && echo "RUNTIME:php" [ -f mix.exs ] && echo "RUNTIME:elixir" +[ -f pom.xml ] && echo "RUNTIME:jvm BUILD:maven" +{ [ -f build.gradle ] || [ -f build.gradle.kts ]; } && echo "RUNTIME:jvm BUILD:gradle" # Detect sub-frameworks [ -f Gemfile ] && grep -q "rails" Gemfile 2>/dev/null && echo "FRAMEWORK:rails" [ -f package.json ] && grep -q '"next"' package.json 2>/dev/null && echo "FRAMEWORK:nextjs" -# Check for existing test infrastructure -ls jest.config.* vitest.config.* playwright.config.* .rspec pytest.ini pyproject.toml phpunit.xml 2>/dev/null -ls -d test/ tests/ spec/ __tests__/ cypress/ e2e/ 2>/dev/null +# Existing test path — config files, declared scripts, AND test FILES. +# A project with real tests and no config file is the common miss. +ls jest.config.* vitest.config.* playwright.config.* .rspec pytest.ini tox.ini phpunit.xml* 2>/dev/null +[ -f package.json ] && grep -q '"test"[[:space:]]*:' package.json && echo "SCRIPT:package.json test" +[ -f Makefile ] && grep -qE '^(test|check):' Makefile && echo "TARGET:make test" +[ -f pyproject.toml ] && grep -q "pytest" pyproject.toml && echo "CONFIG:pyproject pytest" +git ls-files | grep -cE '(^|/)(tests?|spec|__tests__)/|(^|/)tests?\.py$|(^|/)test_[^/]+\.py$|_test\.(go|py|rb|ts|js|exs)$|\.(test|spec)\.[jt]sx?$|_spec\.rb$|Test\.(java|kt)$' | sed 's/^/TESTFILES:/' +# Rust keeps unit tests inside src/, so file names alone miss them +[ -f Cargo.toml ] && git grep -lF '#[test]' -- 'src' >/dev/null 2>&1 && echo "TESTS:rust in-source" # Check opt-out marker [ -f .gstack/no-test-bootstrap ] && echo "BOOTSTRAP_DECLINED" ``` -**If test framework detected** (config files or test directories found): -Print "Test framework detected: {name} ({N} existing tests). Skipping bootstrap." +Map the markers to the command you will OFFER — never to one you run on a guess: + +| Marker | Ecosystem | Candidate command to offer | +|--------|-----------|----------------------------| +| `manage.py` | Django | `python manage.py test` (or `pytest` when pytest-django is in the deps) | +| `pytest.ini` / `tox.ini` / pytest in `pyproject.toml` / `test_*.py` | Python | `pytest` | +| `go.mod` (+ any `*_test.go`) | Go | `go test ./...` | +| `Cargo.toml` | Rust | `cargo test` | +| `pom.xml` | JVM (Maven) | `mvn test` | +| `build.gradle` / `build.gradle.kts` | JVM (Gradle) | `./gradlew test` | +| `Gemfile` / `Rakefile` / `.rspec` | Ruby | `bundle exec rspec`, `bin/rails test`, or `rake test` | +| `mix.exs` | Elixir | `mix test` | +| `composer.json` | PHP | `composer test` or `./vendor/bin/phpunit` | +| `package.json` with a `test` script | Node | that script, run with the package manager the lockfile names | +| `Makefile` with a `test:` target | any | `make test` | + +**If ANY existing-test evidence appears** (a config file, a declared test script or make target, a nonzero `TESTFILES:` count, or `TESTS:rust in-source`): the project has tests. **Do NOT bootstrap.** Print "Existing tests detected: {the evidence}." Then get the command the same way Step 5 does — CLAUDE.md/TESTING.md if documented, otherwise AskUserQuestion offering the candidates from the table above plus "Other", and persist the answer to CLAUDE.md's `## Testing` section so it is never asked again. When the ecosystem ships a runner (Django, Go, Rust, Elixir, Maven/Gradle), that runner is the candidate — never install a second framework beside a working one. Read 2-3 existing test files to learn conventions (naming, imports, assertion style, setup patterns). Store conventions as prose context for use in Phase 8e.5 or Step 7. **Skip the rest of bootstrap.** +Absent config files and absent `tests/` directories are NOT evidence of "no tests": Django keeps tests in `/tests.py`, Go in `*_test.go` beside the source, Rust in `#[test]` blocks inside `src/`. A green `python manage.py test` with no `pytest.ini` is a tested project, not a bootstrap candidate. + **If BOOTSTRAP_DECLINED** appears: Print "Test bootstrap previously declined — skipping." **Skip the rest of bootstrap.** -**If NO runtime detected** (no config files found): Use AskUserQuestion: +**If NO ecosystem marker matched:** Use AskUserQuestion: "I couldn't detect your project's language. What runtime are you using?" Options: A) Node.js/TypeScript B) Ruby/Rails C) Python D) Go E) Rust F) PHP G) Elixir H) This project doesn't need tests. +If the runtime you need isn't listed, offer "Other" and take the runtime plus the test command as free text. If user picks H → write `.gstack/no-test-bootstrap` and continue without tests. -**If runtime detected but no test framework — bootstrap:** +**If an ecosystem matched but there is no existing-test evidence at all — bootstrap:** ### B2. Research best practices @@ -941,7 +970,9 @@ If WebSearch is unavailable, use this built-in knowledge table: | Node.js | vitest + @testing-library | jest + @testing-library | | Next.js | vitest + @testing-library/react + playwright | jest + cypress | | Python | pytest + pytest-cov | unittest | +| Django | pytest + pytest-django | Django's built-in `manage.py test` (unittest) | | Go | stdlib testing + testify | stdlib only | +| JVM (Maven/Gradle) | JUnit 5 + AssertJ | JUnit 5 only | | Rust | cargo test (built-in) + mockall | — | | PHP | phpunit + mockery | pest | | Elixir | ExUnit (built-in) + ex_machina | — | diff --git a/plan-eng-review/sections/review-sections.md b/plan-eng-review/sections/review-sections.md index 7592f0a70..caae69a7e 100644 --- a/plan-eng-review/sections/review-sections.md +++ b/plan-eng-review/sections/review-sections.md @@ -147,15 +147,20 @@ Before analyzing coverage, detect the project's test framework: ```bash setopt +o nomatch 2>/dev/null || true # zsh compat -# Detect project runtime -[ -f Gemfile ] && echo "RUNTIME:ruby" +# Detect project runtime (markers are evidence, not commands to run blind) +[ -f manage.py ] && echo "RUNTIME:python FRAMEWORK:django" +{ [ -f pyproject.toml ] || [ -f pytest.ini ] || [ -f tox.ini ] || [ -f setup.cfg ] || [ -f requirements.txt ]; } && echo "RUNTIME:python" +[ -f Gemfile ] || [ -f Rakefile ] || [ -f .rspec ] && echo "RUNTIME:ruby" [ -f package.json ] && echo "RUNTIME:node" -[ -f requirements.txt ] || [ -f pyproject.toml ] && echo "RUNTIME:python" [ -f go.mod ] && echo "RUNTIME:go" [ -f Cargo.toml ] && echo "RUNTIME:rust" -# Check for existing test infrastructure -ls jest.config.* vitest.config.* playwright.config.* cypress.config.* .rspec pytest.ini phpunit.xml 2>/dev/null -ls -d test/ tests/ spec/ __tests__/ cypress/ e2e/ 2>/dev/null +[ -f pom.xml ] && echo "RUNTIME:jvm BUILD:maven" +{ [ -f build.gradle ] || [ -f build.gradle.kts ]; } && echo "RUNTIME:jvm BUILD:gradle" +# Check for existing test infrastructure — config files, scripts, AND test files +ls jest.config.* vitest.config.* playwright.config.* cypress.config.* .rspec pytest.ini tox.ini phpunit.xml 2>/dev/null +[ -f package.json ] && grep -q '"test"[[:space:]]*:' package.json && echo "SCRIPT:package.json test" +[ -f Makefile ] && grep -qE '^(test|check):' Makefile && echo "TARGET:make test" +git ls-files | grep -cE '(^|/)(tests?|spec|__tests__)/|(^|/)tests?\.py$|(^|/)test_[^/]+\.py$|_test\.(go|py|rb|ts|js|exs)$|\.(test|spec)\.[jt]sx?$|_spec\.rb$|Test\.(java|kt)$' | sed 's/^/TESTFILES:/' ``` 3. **If no framework detected:** still produce the coverage diagram, but skip test generation. diff --git a/qa/SKILL.md b/qa/SKILL.md index e7b6d026f..f2f212c4d 100644 --- a/qa/SKILL.md +++ b/qa/SKILL.md @@ -935,41 +935,70 @@ If `NEEDS_SETUP`: ## Test Framework Bootstrap -**Detect existing test framework and project runtime:** +**Read the project's CLAUDE.md (and TESTING.md if present) FIRST.** If it documents a test command, the project already told you: no detection, no bootstrap. Skip the rest of bootstrap and use that command in Step 5. + +**Otherwise gather markers. Every marker below is EVIDENCE for the question you ask — never a command to run blind.** A marker tells you which ecosystem you're in and which command to OFFER. It does not tell you the command works. Do not execute a candidate test command to "check" it: a probe on a project that never had that runner fails loudly and teaches you nothing, and installing a second framework over a working one is worse. ```bash setopt +o nomatch 2>/dev/null || true # zsh compat -# Detect project runtime -[ -f Gemfile ] && echo "RUNTIME:ruby" +# Definitive ecosystem markers (presence = ecosystem, NOT a command to run) +[ -f manage.py ] && echo "RUNTIME:python FRAMEWORK:django MARKER:manage.py" +{ [ -f pyproject.toml ] || [ -f pytest.ini ] || [ -f tox.ini ] || [ -f setup.cfg ] || [ -f requirements.txt ]; } && echo "RUNTIME:python" +[ -f Gemfile ] || [ -f Rakefile ] || [ -f .rspec ] && echo "RUNTIME:ruby" [ -f package.json ] && echo "RUNTIME:node" -[ -f requirements.txt ] || [ -f pyproject.toml ] && echo "RUNTIME:python" [ -f go.mod ] && echo "RUNTIME:go" [ -f Cargo.toml ] && echo "RUNTIME:rust" [ -f composer.json ] && echo "RUNTIME:php" [ -f mix.exs ] && echo "RUNTIME:elixir" +[ -f pom.xml ] && echo "RUNTIME:jvm BUILD:maven" +{ [ -f build.gradle ] || [ -f build.gradle.kts ]; } && echo "RUNTIME:jvm BUILD:gradle" # Detect sub-frameworks [ -f Gemfile ] && grep -q "rails" Gemfile 2>/dev/null && echo "FRAMEWORK:rails" [ -f package.json ] && grep -q '"next"' package.json 2>/dev/null && echo "FRAMEWORK:nextjs" -# Check for existing test infrastructure -ls jest.config.* vitest.config.* playwright.config.* .rspec pytest.ini pyproject.toml phpunit.xml 2>/dev/null -ls -d test/ tests/ spec/ __tests__/ cypress/ e2e/ 2>/dev/null +# Existing test path — config files, declared scripts, AND test FILES. +# A project with real tests and no config file is the common miss. +ls jest.config.* vitest.config.* playwright.config.* .rspec pytest.ini tox.ini phpunit.xml* 2>/dev/null +[ -f package.json ] && grep -q '"test"[[:space:]]*:' package.json && echo "SCRIPT:package.json test" +[ -f Makefile ] && grep -qE '^(test|check):' Makefile && echo "TARGET:make test" +[ -f pyproject.toml ] && grep -q "pytest" pyproject.toml && echo "CONFIG:pyproject pytest" +git ls-files | grep -cE '(^|/)(tests?|spec|__tests__)/|(^|/)tests?\.py$|(^|/)test_[^/]+\.py$|_test\.(go|py|rb|ts|js|exs)$|\.(test|spec)\.[jt]sx?$|_spec\.rb$|Test\.(java|kt)$' | sed 's/^/TESTFILES:/' +# Rust keeps unit tests inside src/, so file names alone miss them +[ -f Cargo.toml ] && git grep -lF '#[test]' -- 'src' >/dev/null 2>&1 && echo "TESTS:rust in-source" # Check opt-out marker [ -f .gstack/no-test-bootstrap ] && echo "BOOTSTRAP_DECLINED" ``` -**If test framework detected** (config files or test directories found): -Print "Test framework detected: {name} ({N} existing tests). Skipping bootstrap." +Map the markers to the command you will OFFER — never to one you run on a guess: + +| Marker | Ecosystem | Candidate command to offer | +|--------|-----------|----------------------------| +| `manage.py` | Django | `python manage.py test` (or `pytest` when pytest-django is in the deps) | +| `pytest.ini` / `tox.ini` / pytest in `pyproject.toml` / `test_*.py` | Python | `pytest` | +| `go.mod` (+ any `*_test.go`) | Go | `go test ./...` | +| `Cargo.toml` | Rust | `cargo test` | +| `pom.xml` | JVM (Maven) | `mvn test` | +| `build.gradle` / `build.gradle.kts` | JVM (Gradle) | `./gradlew test` | +| `Gemfile` / `Rakefile` / `.rspec` | Ruby | `bundle exec rspec`, `bin/rails test`, or `rake test` | +| `mix.exs` | Elixir | `mix test` | +| `composer.json` | PHP | `composer test` or `./vendor/bin/phpunit` | +| `package.json` with a `test` script | Node | that script, run with the package manager the lockfile names | +| `Makefile` with a `test:` target | any | `make test` | + +**If ANY existing-test evidence appears** (a config file, a declared test script or make target, a nonzero `TESTFILES:` count, or `TESTS:rust in-source`): the project has tests. **Do NOT bootstrap.** Print "Existing tests detected: {the evidence}." Then get the command the same way Step 5 does — CLAUDE.md/TESTING.md if documented, otherwise AskUserQuestion offering the candidates from the table above plus "Other", and persist the answer to CLAUDE.md's `## Testing` section so it is never asked again. When the ecosystem ships a runner (Django, Go, Rust, Elixir, Maven/Gradle), that runner is the candidate — never install a second framework beside a working one. Read 2-3 existing test files to learn conventions (naming, imports, assertion style, setup patterns). Store conventions as prose context for use in Phase 8e.5 or Step 7. **Skip the rest of bootstrap.** +Absent config files and absent `tests/` directories are NOT evidence of "no tests": Django keeps tests in `/tests.py`, Go in `*_test.go` beside the source, Rust in `#[test]` blocks inside `src/`. A green `python manage.py test` with no `pytest.ini` is a tested project, not a bootstrap candidate. + **If BOOTSTRAP_DECLINED** appears: Print "Test bootstrap previously declined — skipping." **Skip the rest of bootstrap.** -**If NO runtime detected** (no config files found): Use AskUserQuestion: +**If NO ecosystem marker matched:** Use AskUserQuestion: "I couldn't detect your project's language. What runtime are you using?" Options: A) Node.js/TypeScript B) Ruby/Rails C) Python D) Go E) Rust F) PHP G) Elixir H) This project doesn't need tests. +If the runtime you need isn't listed, offer "Other" and take the runtime plus the test command as free text. If user picks H → write `.gstack/no-test-bootstrap` and continue without tests. -**If runtime detected but no test framework — bootstrap:** +**If an ecosystem matched but there is no existing-test evidence at all — bootstrap:** ### B2. Research best practices @@ -985,7 +1014,9 @@ If WebSearch is unavailable, use this built-in knowledge table: | Node.js | vitest + @testing-library | jest + @testing-library | | Next.js | vitest + @testing-library/react + playwright | jest + cypress | | Python | pytest + pytest-cov | unittest | +| Django | pytest + pytest-django | Django's built-in `manage.py test` (unittest) | | Go | stdlib testing + testify | stdlib only | +| JVM (Maven/Gradle) | JUnit 5 + AssertJ | JUnit 5 only | | Rust | cargo test (built-in) + mockall | — | | PHP | phpunit + mockery | pest | | Elixir | ExUnit (built-in) + ex_machina | — | diff --git a/setup-gbrain/SKILL.md b/setup-gbrain/SKILL.md index fe2bbad75..4ddbf1f38 100644 --- a/setup-gbrain/SKILL.md +++ b/setup-gbrain/SKILL.md @@ -888,11 +888,11 @@ mv "$HOME/.gbrain/config.json" "$BACKUP" # gstack default: voyage-code-3 (1024d) when VOYAGE_API_KEY is set — best for # code retrieval. Without the key, fall back to gbrain's own auto-selected # embedding provider chain (OpenAI 1536d when OPENAI_API_KEY is present, etc.). -GBRAIN_EMBED_FLAGS="" +set -- # flags ride the positional params — unquoted $VAR breaks under zsh word-splitting (#1798) if [ -n "${VOYAGE_API_KEY:-}" ]; then - GBRAIN_EMBED_FLAGS="--embedding-model voyage:voyage-code-3 --embedding-dimensions 1024" + set -- --embedding-model voyage:voyage-code-3 --embedding-dimensions 1024 fi -if ! gbrain init --pglite --json $GBRAIN_EMBED_FLAGS; then +if ! gbrain init --pglite --json "$@"; then # Restore on failure mv "$BACKUP" "$HOME/.gbrain/config.json" echo "gbrain init failed. Your previous config was restored at $HOME/.gbrain/config.json." >&2 @@ -1102,11 +1102,11 @@ Then follow the same secret-read + verify + init flow as Path 1. # gstack default: voyage-code-3 (1024d) when VOYAGE_API_KEY is set — code # retrieval beats general-purpose embeddings on real code queries (validated # A/B). Without the key, gbrain auto-selects (OpenAI 1536d when available). -GBRAIN_EMBED_FLAGS="" +set -- # flags ride the positional params — unquoted $VAR breaks under zsh word-splitting (#1798) if [ -n "${VOYAGE_API_KEY:-}" ]; then - GBRAIN_EMBED_FLAGS="--embedding-model voyage:voyage-code-3 --embedding-dimensions 1024" + set -- --embedding-model voyage:voyage-code-3 --embedding-dimensions 1024 fi -gbrain init --pglite --json $GBRAIN_EMBED_FLAGS +gbrain init --pglite --json "$@" ``` Done. No network, no secrets (beyond Voyage embedding API calls during sync, if @@ -1194,11 +1194,11 @@ fi # VOYAGE_API_KEY is set. It wins the A/B over voyage-4-large and OpenAI # text-embedding-3-large on this codebase's symbol queries. Falls back to # gbrain's auto-selected provider when the key isn't present. -GBRAIN_EMBED_FLAGS="" +set -- # flags ride the positional params — unquoted $VAR breaks under zsh word-splitting (#1798) if [ -n "${VOYAGE_API_KEY:-}" ]; then - GBRAIN_EMBED_FLAGS="--embedding-model voyage:voyage-code-3 --embedding-dimensions 1024" + set -- --embedding-model voyage:voyage-code-3 --embedding-dimensions 1024 fi -if ! gbrain init --pglite --json $GBRAIN_EMBED_FLAGS; then +if ! gbrain init --pglite --json "$@"; then if [ -n "${BACKUP:-}" ] && [ -f "$BACKUP" ]; then mv "$BACKUP" "$HOME/.gbrain/config.json"; fi echo "gbrain init failed. Existing config (if any) was restored. PGLite at ~/.gbrain/pglite/ may be in a partial state — \`rm -rf ~/.gbrain/pglite\` to reset." >&2 echo "Continuing setup without local code search; you can re-run /setup-gbrain to retry." >&2 diff --git a/ship/sections/pr-body.md b/ship/sections/pr-body.md index 90888b7a9..742ba0c57 100644 --- a/ship/sections/pr-body.md +++ b/ship/sections/pr-body.md @@ -43,6 +43,8 @@ glab mr view -F json 2>/dev/null | jq -r 'if .state == "opened" then "MR_EXISTS" If an **open** PR/MR already exists: **update** the PR body using `gh pr edit --body-file "$PR_BODY_FILE"` (GitHub) or `glab mr update -d ...` (GitLab). Always regenerate the PR body from scratch using this run's fresh results (test output, coverage audit, review findings, adversarial review, TODOS summary, documentation_section from Step 18). Never reuse stale PR body content from a prior run. **Run the same redaction scan-at-sink (PR body + title) as the create path (Step 19) before editing — scan the temp file, then `gh pr edit --body-file` from it.** +**REST fallback (#1079):** on some repos `gh pr edit` hard-errors with a GraphQL deprecation mentioning `repository.pullRequest.projectCards` ("Projects (classic) is being deprecated..."). That is a `gh` GraphQL-path problem, not a permissions problem — do not re-ask for auth. Fall back to the REST endpoint, which never touches the deprecated field, using the SAME already-scanned temp file: `PR_NUMBER=$(gh pr view --json number -q .number)` then `gh api "repos/{owner}/{repo}/pulls/$PR_NUMBER" -X PATCH -F body=@"$PR_BODY_FILE"` for the body, and `gh api "repos/{owner}/{repo}/pulls/$PR_NUMBER" -X PATCH -f title="$NEW_TITLE"` when the title edit below hits the same error. Verify with the same self-checks as the primary path. + **Always update the PR title to start with `v$NEW_VERSION`.** PR titles use the workspace-aware format `v : ` — version ALWAYS first, no exceptions, no "custom title kept intentionally" escape hatch. The shared helper `bin/gstack-pr-title-rewrite.sh` is the single source of truth for the rule. 1. Read the current title: `CURRENT=$(gh pr view --json title -q .title)` (or `glab mr view -F json | jq -r .title`). diff --git a/ship/sections/test-coverage.md b/ship/sections/test-coverage.md index 6c916a7f0..1963d0aef 100644 --- a/ship/sections/test-coverage.md +++ b/ship/sections/test-coverage.md @@ -19,15 +19,20 @@ Before analyzing coverage, detect the project's test framework: ```bash setopt +o nomatch 2>/dev/null || true # zsh compat -# Detect project runtime -[ -f Gemfile ] && echo "RUNTIME:ruby" +# Detect project runtime (markers are evidence, not commands to run blind) +[ -f manage.py ] && echo "RUNTIME:python FRAMEWORK:django" +{ [ -f pyproject.toml ] || [ -f pytest.ini ] || [ -f tox.ini ] || [ -f setup.cfg ] || [ -f requirements.txt ]; } && echo "RUNTIME:python" +[ -f Gemfile ] || [ -f Rakefile ] || [ -f .rspec ] && echo "RUNTIME:ruby" [ -f package.json ] && echo "RUNTIME:node" -[ -f requirements.txt ] || [ -f pyproject.toml ] && echo "RUNTIME:python" [ -f go.mod ] && echo "RUNTIME:go" [ -f Cargo.toml ] && echo "RUNTIME:rust" -# Check for existing test infrastructure -ls jest.config.* vitest.config.* playwright.config.* cypress.config.* .rspec pytest.ini phpunit.xml 2>/dev/null -ls -d test/ tests/ spec/ __tests__/ cypress/ e2e/ 2>/dev/null +[ -f pom.xml ] && echo "RUNTIME:jvm BUILD:maven" +{ [ -f build.gradle ] || [ -f build.gradle.kts ]; } && echo "RUNTIME:jvm BUILD:gradle" +# Check for existing test infrastructure — config files, scripts, AND test files +ls jest.config.* vitest.config.* playwright.config.* cypress.config.* .rspec pytest.ini tox.ini phpunit.xml 2>/dev/null +[ -f package.json ] && grep -q '"test"[[:space:]]*:' package.json && echo "SCRIPT:package.json test" +[ -f Makefile ] && grep -qE '^(test|check):' Makefile && echo "TARGET:make test" +git ls-files | grep -cE '(^|/)(tests?|spec|__tests__)/|(^|/)tests?\.py$|(^|/)test_[^/]+\.py$|_test\.(go|py|rb|ts|js|exs)$|\.(test|spec)\.[jt]sx?$|_spec\.rb$|Test\.(java|kt)$' | sed 's/^/TESTFILES:/' ``` 3. **If no framework detected:** falls through to the Test Framework Bootstrap step (Step 4) which handles full setup. diff --git a/ship/sections/tests.md b/ship/sections/tests.md index e9d4dd819..2b93deab1 100644 --- a/ship/sections/tests.md +++ b/ship/sections/tests.md @@ -4,41 +4,70 @@ ## Test Framework Bootstrap -**Detect existing test framework and project runtime:** +**Read the project's CLAUDE.md (and TESTING.md if present) FIRST.** If it documents a test command, the project already told you: no detection, no bootstrap. Skip the rest of bootstrap and use that command in Step 5. + +**Otherwise gather markers. Every marker below is EVIDENCE for the question you ask — never a command to run blind.** A marker tells you which ecosystem you're in and which command to OFFER. It does not tell you the command works. Do not execute a candidate test command to "check" it: a probe on a project that never had that runner fails loudly and teaches you nothing, and installing a second framework over a working one is worse. ```bash setopt +o nomatch 2>/dev/null || true # zsh compat -# Detect project runtime -[ -f Gemfile ] && echo "RUNTIME:ruby" +# Definitive ecosystem markers (presence = ecosystem, NOT a command to run) +[ -f manage.py ] && echo "RUNTIME:python FRAMEWORK:django MARKER:manage.py" +{ [ -f pyproject.toml ] || [ -f pytest.ini ] || [ -f tox.ini ] || [ -f setup.cfg ] || [ -f requirements.txt ]; } && echo "RUNTIME:python" +[ -f Gemfile ] || [ -f Rakefile ] || [ -f .rspec ] && echo "RUNTIME:ruby" [ -f package.json ] && echo "RUNTIME:node" -[ -f requirements.txt ] || [ -f pyproject.toml ] && echo "RUNTIME:python" [ -f go.mod ] && echo "RUNTIME:go" [ -f Cargo.toml ] && echo "RUNTIME:rust" [ -f composer.json ] && echo "RUNTIME:php" [ -f mix.exs ] && echo "RUNTIME:elixir" +[ -f pom.xml ] && echo "RUNTIME:jvm BUILD:maven" +{ [ -f build.gradle ] || [ -f build.gradle.kts ]; } && echo "RUNTIME:jvm BUILD:gradle" # Detect sub-frameworks [ -f Gemfile ] && grep -q "rails" Gemfile 2>/dev/null && echo "FRAMEWORK:rails" [ -f package.json ] && grep -q '"next"' package.json 2>/dev/null && echo "FRAMEWORK:nextjs" -# Check for existing test infrastructure -ls jest.config.* vitest.config.* playwright.config.* .rspec pytest.ini pyproject.toml phpunit.xml 2>/dev/null -ls -d test/ tests/ spec/ __tests__/ cypress/ e2e/ 2>/dev/null +# Existing test path — config files, declared scripts, AND test FILES. +# A project with real tests and no config file is the common miss. +ls jest.config.* vitest.config.* playwright.config.* .rspec pytest.ini tox.ini phpunit.xml* 2>/dev/null +[ -f package.json ] && grep -q '"test"[[:space:]]*:' package.json && echo "SCRIPT:package.json test" +[ -f Makefile ] && grep -qE '^(test|check):' Makefile && echo "TARGET:make test" +[ -f pyproject.toml ] && grep -q "pytest" pyproject.toml && echo "CONFIG:pyproject pytest" +git ls-files | grep -cE '(^|/)(tests?|spec|__tests__)/|(^|/)tests?\.py$|(^|/)test_[^/]+\.py$|_test\.(go|py|rb|ts|js|exs)$|\.(test|spec)\.[jt]sx?$|_spec\.rb$|Test\.(java|kt)$' | sed 's/^/TESTFILES:/' +# Rust keeps unit tests inside src/, so file names alone miss them +[ -f Cargo.toml ] && git grep -lF '#[test]' -- 'src' >/dev/null 2>&1 && echo "TESTS:rust in-source" # Check opt-out marker [ -f .gstack/no-test-bootstrap ] && echo "BOOTSTRAP_DECLINED" ``` -**If test framework detected** (config files or test directories found): -Print "Test framework detected: {name} ({N} existing tests). Skipping bootstrap." +Map the markers to the command you will OFFER — never to one you run on a guess: + +| Marker | Ecosystem | Candidate command to offer | +|--------|-----------|----------------------------| +| `manage.py` | Django | `python manage.py test` (or `pytest` when pytest-django is in the deps) | +| `pytest.ini` / `tox.ini` / pytest in `pyproject.toml` / `test_*.py` | Python | `pytest` | +| `go.mod` (+ any `*_test.go`) | Go | `go test ./...` | +| `Cargo.toml` | Rust | `cargo test` | +| `pom.xml` | JVM (Maven) | `mvn test` | +| `build.gradle` / `build.gradle.kts` | JVM (Gradle) | `./gradlew test` | +| `Gemfile` / `Rakefile` / `.rspec` | Ruby | `bundle exec rspec`, `bin/rails test`, or `rake test` | +| `mix.exs` | Elixir | `mix test` | +| `composer.json` | PHP | `composer test` or `./vendor/bin/phpunit` | +| `package.json` with a `test` script | Node | that script, run with the package manager the lockfile names | +| `Makefile` with a `test:` target | any | `make test` | + +**If ANY existing-test evidence appears** (a config file, a declared test script or make target, a nonzero `TESTFILES:` count, or `TESTS:rust in-source`): the project has tests. **Do NOT bootstrap.** Print "Existing tests detected: {the evidence}." Then get the command the same way Step 5 does — CLAUDE.md/TESTING.md if documented, otherwise AskUserQuestion offering the candidates from the table above plus "Other", and persist the answer to CLAUDE.md's `## Testing` section so it is never asked again. When the ecosystem ships a runner (Django, Go, Rust, Elixir, Maven/Gradle), that runner is the candidate — never install a second framework beside a working one. Read 2-3 existing test files to learn conventions (naming, imports, assertion style, setup patterns). Store conventions as prose context for use in Phase 8e.5 or Step 7. **Skip the rest of bootstrap.** +Absent config files and absent `tests/` directories are NOT evidence of "no tests": Django keeps tests in `/tests.py`, Go in `*_test.go` beside the source, Rust in `#[test]` blocks inside `src/`. A green `python manage.py test` with no `pytest.ini` is a tested project, not a bootstrap candidate. + **If BOOTSTRAP_DECLINED** appears: Print "Test bootstrap previously declined — skipping." **Skip the rest of bootstrap.** -**If NO runtime detected** (no config files found): Use AskUserQuestion: +**If NO ecosystem marker matched:** Use AskUserQuestion: "I couldn't detect your project's language. What runtime are you using?" Options: A) Node.js/TypeScript B) Ruby/Rails C) Python D) Go E) Rust F) PHP G) Elixir H) This project doesn't need tests. +If the runtime you need isn't listed, offer "Other" and take the runtime plus the test command as free text. If user picks H → write `.gstack/no-test-bootstrap` and continue without tests. -**If runtime detected but no test framework — bootstrap:** +**If an ecosystem matched but there is no existing-test evidence at all — bootstrap:** ### B2. Research best practices @@ -54,7 +83,9 @@ If WebSearch is unavailable, use this built-in knowledge table: | Node.js | vitest + @testing-library | jest + @testing-library | | Next.js | vitest + @testing-library/react + playwright | jest + cypress | | Python | pytest + pytest-cov | unittest | +| Django | pytest + pytest-django | Django's built-in `manage.py test` (unittest) | | Go | stdlib testing + testify | stdlib only | +| JVM (Maven/Gradle) | JUnit 5 + AssertJ | JUnit 5 only | | Rust | cargo test (built-in) + mockall | — | | PHP | phpunit + mockery | pest | | Elixir | ExUnit (built-in) + ex_machina | — |