fix(scope,review,ship): recognise Dart and Flutter in diff scope and test detection (#3284)

Port of arlophoenix's #3284 onto current main, squashed:
- gstack-diff-scope: .dart is frontend and backend; pubspec.lock is config.
- lib/frontend-scope.ts mirrors the frontend half; gstack-design-detect
  never sends .dart to the impeccable engine.
- Test bootstrap, coverage audits and review-army detect Dart/Flutter
  (RUNTIME:dart, FRAMEWORK:flutter, dart_test.yaml, *_test.dart).
- Adversarial DIFF_SIZE sums git diff --numstat.

On top of the contributor's diff: shorten the bootstrap's ecosystem-marker
comment so /design-review stays at its SKILL.md size pin, lower the 31 pins
that shrank, and add the CHANGELOG entry.

Co-authored-by: arlophoenix <arlo.phoenix@gmail.com>
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
committed 2026-10-19 18:42:29 +00:00
1 parent 9cf105ccff
commit f4fb0ebabe
22 files changed
+254 -94

No files matched your search

+13
View File
@@ -1,5 +1,18 @@
# Changelog
## [Unreleased]
**Dart and Flutter projects get the right reviewers and the right test command.** A Dart change matched no diff-scope category except its test file, so a Flutter widget PR ran only the testing pass and skipped design, performance and security. Contributed by @arlophoenix (#3284).
### Fixed
- **`gstack-diff-scope` counts `.dart` as both frontend and backend.** The extension cannot tell a Flutter widget from a repository class or a pure-Dart server, and a skipped reviewer is invisible, so both specialists run. `pubspec.lock` (at the root or in a workspace package) is config. `lib/frontend-scope.ts` mirrors the frontend half, and `gstack-design-detect` still never hands a `.dart` file to the impeccable engine, which reads web sources only.
- **Test detection knows Dart.** `/ship`'s bootstrap and coverage audit, `/design-review` and `/plan-eng-review` print `RUNTIME:dart` for a `pubspec.yaml` and `FRAMEWORK:flutter` for an indented `sdk: flutter` dependency (a commented-out one or an `environment.sdk` constraint does not count). They treat `dart_test.yaml` and `*_test.dart` files anywhere as existing tests and offer `flutter test` or `dart test`. Review-army reports `STACK: dart` and `TEST_FW: dart-test` or `flutter-test`.
- **Adversarial `DIFF_SIZE` sums `git diff --numstat`,** the same count review-army's `DIFF_LINES` uses, instead of parsing the `--stat` summary line. Binary files count as zero.
### Changed
- The test-bootstrap prose is shorter so the generated skills stay inside the parity size cap and `/design-review` stays at its SKILL.md size pin. The rules it states are unchanged. `/design-review`, `/plan-eng-review` and `/review` shrink, so their pins in `test/fixtures/skill-md-size-allowlist.json` go down (31 entries).
- `test/frontend-scope.test.ts`'s bash parity test gets a 30 s budget. It spawns git six times and the scope script once per sample, and it already ran past Bun's 5 s default on a 4-core machine before this change.
## [1.91.149.0] - 2026-10-19
**Paid eval cases can now run on the Codex host, and the Monday census runs two arms: Claude Opus 5.5 and Codex gpt-6.1-sol.** `EVALS_HOST=codex` runs the 52 adapter-only gate cases through a full Codex runtime per case. On its first pilot gpt-6.1-sol passed 48 of 52 with no retries, at $4.63 for the 52 cases, against $28.93 for the same cases on Fable (0.16×). Claude-only cases are skipped on Codex with a recorded reason and never count as failed. GSTA-49 TM2.
+2 -1
View File
@@ -829,7 +829,8 @@ function resolveTargets(args: ScanArgs, p: Probe): { targets: string[]; refusedB
runZ(['diff', '-z', '--name-only', '--diff-filter=ACMR', 'HEAD']);
runZ(['ls-files', '-z', '--others', '--exclude-standard']);
for (const rel of [...files].sort()) {
if (!isFrontendPath(rel)) continue;
// impeccable reads web sources; a Dart file is frontend scope but not a scan target.
if (!isFrontendPath(rel) || rel.endsWith('.dart')) continue;
const abs = path.join(top, rel);
try { if (fs.lstatSync(abs).isSymbolicLink()) { refuse(rel, 'symlink named by git is never scanned'); continue; } } catch { continue; }
const real = realpathOrNull(abs);
+9
View File
@@ -104,6 +104,11 @@ for f in ${FILES_LIST[@]+"${FILES_LIST[@]}"}; do
*.tsx|*.jsx|*.vue|*.svelte|*.astro) m_frontend=true ;;
*.erb|*.haml|*.slim|*.hbs|*.ejs) m_frontend=true ;;
*.html) m_frontend=true ;;
# Dart: the extension cannot tell a Flutter widget from a repository or a
# pure-Dart server, so it is BOTH frontend and backend (BACKEND set below).
# A skipped reviewer is invisible; an extra one (design-lite included) is a
# visible pass that costs a review.
*.dart) m_frontend=true ;;
tailwind.config.*|postcss.config.*) m_frontend=true ;;
app/views/*|*/components/*|styles/*|css/*|app/assets/stylesheets/*) m_frontend=true ;;
esac
@@ -135,6 +140,7 @@ for f in ${FILES_LIST[@]+"${FILES_LIST[@]}"}; do
*.yml|*.yaml) m_config=true ;;
.github/*) m_config=true ;;
requirements.txt|pyproject.toml|go.mod|Cargo.toml|composer.json) m_config=true ;;
pubspec.lock|*/pubspec.lock) m_config=true ;;
esac
# Migrations: database migration files. Bare migrations/* covers a
@@ -169,6 +175,9 @@ for f in ${FILES_LIST[@]+"${FILES_LIST[@]}"}; do
*.ts|*.js|*.mjs|*.cjs|*.mts|*.cts) m_backend=true ;;
esac
fi
case "$f" in
*.dart) m_backend=true ;;
esac
[ "$m_frontend" = true ] && FRONTEND=true
[ "$m_prompts" = true ] && PROMPTS=true
+12 -9
View File
@@ -535,17 +535,18 @@ Label `$B` output with the same evidence lines (`URL=`, `CONSOLE_ERRORS=`, `DIFF
**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.
**Otherwise gather markers. Every marker below is EVIDENCE for the question you ask — never a command to run blind.** A marker names the ecosystem and the command to OFFER; it does not prove the command works. Never execute a candidate to "check" it: probing a runner the project never had fails loudly and teaches nothing.
```bash
setopt +o nomatch 2>/dev/null || true # zsh compat
# Definitive ecosystem markers (presence = ecosystem, NOT a command to run)
# Ecosystem markers
[ -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 go.mod ] && echo "RUNTIME:go"
[ -f Cargo.toml ] && echo "RUNTIME:rust"
[ -f pubspec.yaml ] && echo "RUNTIME:dart"
[ -f composer.json ] && echo "RUNTIME:php"
[ -f mix.exs ] && echo "RUNTIME:elixir"
[ -f pom.xml ] && echo "RUNTIME:jvm BUILD:maven"
@@ -553,14 +554,14 @@ setopt +o nomatch 2>/dev/null || true # zsh compat
# 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"
# 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
grep -qsE '^ +sdk: *.?flutter' pubspec.yaml && echo "FRAMEWORK:flutter"
# Test evidence: config files, declared scripts, AND test FILES (tested projects often lack a config)
ls jest.config.* vitest.config.* playwright.config.* .rspec pytest.ini tox.ini phpunit.xml* dart_test.yaml 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
git ls-files | grep -cE '(^|/)(tests?|spec|__tests__)/|(^|/)tests?\.py$|(^|/)test_[^/]+\.py$|_test\.(go|py|rb|ts|js|exs|dart)$|\.(test|spec)\.[jt]sx?$|_spec\.rb$|Test\.(java|kt)$' | sed 's/^/TESTFILES:/'
# Rust unit tests live in src/; 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"
@@ -574,6 +575,7 @@ Map the markers to the command you will OFFER — never to one you run on a gues
| `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` |
| `pubspec.yaml` | Dart / Flutter | `flutter test` (FRAMEWORK:flutter) or `dart test`; `fvm` prefix if `.fvmrc` |
| `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` |
@@ -582,11 +584,11 @@ Map the markers to the command you will OFFER — never to one you run on a gues
| `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.
**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, Dart, 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 `<app>/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.
Absent config files and absent `tests/` directories are NOT evidence of "no tests": Django keeps tests in `<app>/tests.py`, Go in `*_test.go` beside the source, Rust in `#[test]` blocks inside `src/`.
**If BOOTSTRAP_DECLINED** appears: Print "Test bootstrap previously declined — skipping." **Skip the rest of bootstrap.**
@@ -630,6 +632,7 @@ If Aside is not installed or not running (`command -v aside` prints nothing, or
| Go | stdlib testing + testify | stdlib only |
| JVM (Maven/Gradle) | JUnit 5 + AssertJ | JUnit 5 only |
| Rust | cargo test (built-in) + mockall | — |
| Dart | test / flutter_test + mocktail | mockito |
| PHP | phpunit + mockery | pest |
| Elixir | ExUnit (built-in) + ex_machina | — |
+2 -2
View File
@@ -413,7 +413,7 @@ It runs an 80-item visual audit on your live site — in your Aside browser, so
The self-regulation heuristic is tuned for design work — CSS-only changes get a free pass (they are inherently safe and reversible), but changes to component JSX/TSX files count against the risk budget. Hard cap at 30 fixes. If the risk score exceeds 20%, it stops and asks.
**Design detector.** When you have [impeccable](https://impeccable.style) installed, its engine runs first: on a URL the page's rendered DOM is dumped (linked styles inlined) and scanned; on a feature branch with no URL the changed frontend files are scanned. Every mechanical finding arrives as a `FINDING-NNN` tagged with its rule id (`[nested-cards]`, `[low-contrast]`), and the report closes with `Detector: N → M`. gstack never runs impeccable's installer; when no engine is present it offers, once, to download the engine binary (checksum-pinned, logged in the egress ledger) and remembers the answer; without it the audit is unchanged. `gstack-config set design_detector off` disables the pre-pass.
**Design detector.** When you have [impeccable](https://impeccable.style) installed, its engine runs first: on a URL the page's rendered DOM is dumped (linked styles inlined) and scanned; on a feature branch with no URL the changed frontend files are scanned (`.dart` files excepted: they count as frontend scope, but the engine reads web sources). Every mechanical finding arrives as a `FINDING-NNN` tagged with its rule id (`[nested-cards]`, `[low-contrast]`), and the report closes with `Detector: N → M`. gstack never runs impeccable's installer; when no engine is present it offers, once, to download the engine binary (checksum-pinned, logged in the egress ledger) and remembers the answer; without it the audit is unchanged. `gstack-config set design_detector off` disables the pre-pass.
### Example
@@ -586,7 +586,7 @@ Findings get action, not just listed. Obvious mechanical fixes (dead code, stale
One exception: a shortcut you took deliberately and logged. A `gstack-shortcut(dec-<id>)` marker whose decision id resolves in the decision ledger downgrades the finding to acknowledged debt. An orphan marker — one with no ledger entry behind it — doesn't suppress anything; the gap is reported normally and the marker itself gets flagged.
**Design pass.** When the diff touches frontend files, the Design specialist reads `review/design-checklist.md`, which is generated from `lib/design-catalog.ts`, so `/review`, `/ship`, and `/design-review` flag the same patterns under the same rule ids. If you have [impeccable](https://impeccable.style) installed, its engine scans the changed frontend files first: its rows bucket by tier (auto-fix, ask, possible), a detector hit and a checklist hit at the same file:line collapse into one row, and your repo's `.impeccable/config*.json` ignores are read as settled decisions. Without it, the checklist pass runs alone.
**Design pass.** When the diff touches frontend files, the Design specialist reads `review/design-checklist.md`, which is generated from `lib/design-catalog.ts`, so `/review`, `/ship`, and `/design-review` flag the same patterns under the same rule ids. If you have [impeccable](https://impeccable.style) installed, its engine scans the changed frontend files first (`.dart` files get the checklist pass only): its rows bucket by tier (auto-fix, ask, possible), a detector hit and a checklist hit at the same file:line collapse into one row, and your repo's `.impeccable/config*.json` ignores are read as settled decisions. Without it, the checklist pass runs alone.
### Example
+3
View File
@@ -11,6 +11,9 @@ const EXTENSIONS = new Set([
'.tsx', '.jsx', '.vue', '.svelte', '.astro',
'.erb', '.haml', '.slim', '.hbs', '.ejs',
'.html',
// Dart: a Flutter widget and a repository share the extension, so the bash
// script counts .dart as frontend AND backend; only the frontend half lives here.
'.dart',
]);
// Root-level only: the bash arm's glob (`tailwind.config.*`) is matched against the
+5 -4
View File
@@ -585,20 +585,21 @@ Before analyzing coverage, detect the project's test framework:
```bash
setopt +o nomatch 2>/dev/null || true # zsh compat
# Detect project runtime (markers are evidence, not commands to run blind)
# Runtime markers: evidence, not commands to run
[ -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 go.mod ] && echo "RUNTIME:go"
[ -f Cargo.toml ] && echo "RUNTIME:rust"
[ -f pubspec.yaml ] && echo "RUNTIME:dart"
[ -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
# Test infra: configs, scripts, test files
ls jest.config.* vitest.config.* playwright.config.* cypress.config.* .rspec pytest.ini tox.ini phpunit.xml dart_test.yaml 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:/'
git ls-files | grep -cE '(^|/)(tests?|spec|__tests__)/|(^|/)tests?\.py$|(^|/)test_[^/]+\.py$|_test\.(go|py|rb|ts|js|exs|dart)$|\.(test|spec)\.[jt]sx?$|_spec\.rb$|Test\.(java|kt)$' | sed 's/^/TESTFILES:/'
```
3. **If no framework detected:** State that the framework is unknown; continue the diagram and planned assertions. If proposing a new framework, settle that choice through Decision procedure in Test step 5. Reuse an exact prior approval; with no selection proposed, ask no framework question. Do not install a framework or write the proposed tests during this review.
+1 -3
View File
@@ -8,9 +8,7 @@ Every diff gets the Claude adversarial pass. Add Codex when its preflight is rea
```bash
DIFF_BASE=$(git merge-base origin/<base> HEAD)
DIFF_INS=$(git diff "$DIFF_BASE" --stat | tail -1 | grep -oE '[0-9]+ insertion' | grep -oE '[0-9]+' || echo "0")
DIFF_DEL=$(git diff "$DIFF_BASE" --stat | tail -1 | grep -oE '[0-9]+ deletion' | grep -oE '[0-9]+' || echo "0")
DIFF_TOTAL=$((DIFF_INS + DIFF_DEL))
DIFF_TOTAL=$(git diff "$DIFF_BASE" --numstat | awk '{n+=$1+$2} END {print n+0}')
echo "DIFF_SIZE: $DIFF_TOTAL"
```
+2
View File
@@ -14,6 +14,7 @@ STACK=""
[ -f requirements.txt ] || [ -f pyproject.toml ] && STACK="${STACK}python "
[ -f go.mod ] && STACK="${STACK}go "
[ -f Cargo.toml ] && STACK="${STACK}rust "
[ -f pubspec.yaml ] && STACK="${STACK}dart "
echo "STACK: ${STACK:-unknown}"
DIFF_BASE=$(git merge-base origin/<base> HEAD)
DIFF_LINES=$(git diff "$DIFF_BASE" --numstat | awk '{n+=$1+$2} END {print n+0}')
@@ -25,6 +26,7 @@ TEST_FW=""
{ [ -f spec/spec_helper.rb ] || [ -f .rspec ]; } && TEST_FW="rspec"
{ [ -f pytest.ini ] || [ -f conftest.py ]; } && TEST_FW="pytest"
[ -f go.mod ] && TEST_FW="go-test"
[ -f pubspec.yaml ] && TEST_FW="dart-test" && grep -qsE '^ +sdk: *.?flutter' pubspec.yaml && TEST_FW="flutter-test"
echo "TEST_FW: ${TEST_FW:-unknown}"
```
+1 -3
View File
@@ -243,9 +243,7 @@ Every diff gets the ${outsideVoiceFor(ctx).nativeLabel} adversarial pass. Add ${
\`\`\`bash
DIFF_BASE=$(git merge-base origin/<base> HEAD)
DIFF_INS=$(git diff "$DIFF_BASE" --stat | tail -1 | grep -oE '[0-9]+ insertion' | grep -oE '[0-9]+' || echo "0")
DIFF_DEL=$(git diff "$DIFF_BASE" --stat | tail -1 | grep -oE '[0-9]+ deletion' | grep -oE '[0-9]+' || echo "0")
DIFF_TOTAL=$((DIFF_INS + DIFF_DEL))
DIFF_TOTAL=$(git diff "$DIFF_BASE" --numstat | awk '{n+=$1+$2} END {print n+0}')
echo "DIFF_SIZE: $DIFF_TOTAL"
\`\`\`
+2
View File
@@ -33,6 +33,7 @@ STACK=""
[ -f requirements.txt ] || [ -f pyproject.toml ] && STACK="\${STACK}python "
[ -f go.mod ] && STACK="\${STACK}go "
[ -f Cargo.toml ] && STACK="\${STACK}rust "
[ -f pubspec.yaml ] && STACK="\${STACK}dart "
echo "STACK: \${STACK:-unknown}"
DIFF_BASE=$(git merge-base origin/<base> HEAD)
DIFF_LINES=$(git diff "$DIFF_BASE" --numstat | awk '{n+=$1+$2} END {print n+0}')
@@ -44,6 +45,7 @@ TEST_FW=""
{ [ -f spec/spec_helper.rb ] || [ -f .rspec ]; } && TEST_FW="rspec"
{ [ -f pytest.ini ] || [ -f conftest.py ]; } && TEST_FW="pytest"
[ -f go.mod ] && TEST_FW="go-test"
[ -f pubspec.yaml ] && TEST_FW="dart-test" && grep -qsE '^ +sdk: *.?flutter' pubspec.yaml && TEST_FW="flutter-test"
echo "TEST_FW: \${TEST_FW:-unknown}"
\`\`\`
+17 -13
View File
@@ -8,17 +8,18 @@ export function generateTestBootstrap(ctx: TemplateContext): string {
**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.
**Otherwise gather markers. Every marker below is EVIDENCE for the question you ask — never a command to run blind.** A marker names the ecosystem and the command to OFFER; it does not prove the command works. Never execute a candidate to "check" it: probing a runner the project never had fails loudly and teaches nothing.
\`\`\`bash
setopt +o nomatch 2>/dev/null || true # zsh compat
# Definitive ecosystem markers (presence = ecosystem, NOT a command to run)
# Ecosystem markers
[ -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 go.mod ] && echo "RUNTIME:go"
[ -f Cargo.toml ] && echo "RUNTIME:rust"
[ -f pubspec.yaml ] && echo "RUNTIME:dart"
[ -f composer.json ] && echo "RUNTIME:php"
[ -f mix.exs ] && echo "RUNTIME:elixir"
[ -f pom.xml ] && echo "RUNTIME:jvm BUILD:maven"
@@ -26,14 +27,14 @@ setopt +o nomatch 2>/dev/null || true # zsh compat
# 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"
# 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
grep -qsE '^ +sdk: *.?flutter' pubspec.yaml && echo "FRAMEWORK:flutter"
# Test evidence: config files, declared scripts, AND test FILES (tested projects often lack a config)
ls jest.config.* vitest.config.* playwright.config.* .rspec pytest.ini tox.ini phpunit.xml* dart_test.yaml 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
git ls-files | grep -cE '(^|/)(tests?|spec|__tests__)/|(^|/)tests?\\.py$|(^|/)test_[^/]+\\.py$|_test\\.(go|py|rb|ts|js|exs|dart)$|\\.(test|spec)\\.[jt]sx?$|_spec\\.rb$|Test\\.(java|kt)$' | sed 's/^/TESTFILES:/'
# Rust unit tests live in src/; 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"
@@ -47,6 +48,7 @@ Map the markers to the command you will OFFER — never to one you run on a gues
| \`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\` |
| \`pubspec.yaml\` | Dart / Flutter | \`flutter test\` (FRAMEWORK:flutter) or \`dart test\`; \`fvm\` prefix if \`.fvmrc\` |
| \`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\` |
@@ -55,11 +57,11 @@ Map the markers to the command you will OFFER — never to one you run on a gues
| \`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.
**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, Dart, 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 ${ctx.skillName === 'ship' ? 'Step 7' : '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 \`<app>/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.
Absent config files and absent \`tests/\` directories are NOT evidence of "no tests": Django keeps tests in \`<app>/tests.py\`, Go in \`*_test.go\` beside the source, Rust in \`#[test]\` blocks inside \`src/\`.
${ctx.skillName === 'ship'
? '**If BOOTSTRAP_DECLINED** appears:\n- Step 5\'s explicit Add tests choice overrides that marker for this invocation only: continue to runtime detection and B2–B3, including framework approval.\n- Otherwise print "Test bootstrap previously declined — skipping" and **skip the rest of bootstrap**.'
@@ -99,6 +101,7 @@ If Aside is not installed or not running (\`command -v aside\` prints nothing, o
| Go | stdlib testing + testify | stdlib only |
| JVM (Maven/Gradle) | JUnit 5 + AssertJ | JUnit 5 only |
| Rust | cargo test (built-in) + mockall | — |
| Dart | test / flutter_test + mocktail | mockito |
| PHP | phpunit + mockery | pest |
| Elixir | ExUnit (built-in) + ex_machina | — |
@@ -269,20 +272,21 @@ Before analyzing coverage, detect the project's test framework:
\`\`\`bash
setopt +o nomatch 2>/dev/null || true # zsh compat
# Detect project runtime (markers are evidence, not commands to run blind)
# Runtime markers: evidence, not commands to run
[ -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 go.mod ] && echo "RUNTIME:go"
[ -f Cargo.toml ] && echo "RUNTIME:rust"
[ -f pubspec.yaml ] && echo "RUNTIME:dart"
[ -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
# Test infra: configs, scripts, test files
ls jest.config.* vitest.config.* playwright.config.* cypress.config.* .rspec pytest.ini tox.ini phpunit.xml dart_test.yaml 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:/'
git ls-files | grep -cE '(^|/)(tests?|spec|__tests__)/|(^|/)tests?\\.py$|(^|/)test_[^/]+\\.py$|_test\\.(go|py|rb|ts|js|exs|dart)$|\\.(test|spec)\\.[jt]sx?$|_spec\\.rb$|Test\\.(java|kt)$' | sed 's/^/TESTFILES:/'
\`\`\`
3. **If no framework detected:**${mode === 'ship' ? ' use the bootstrap decision already made in Step 4; report diagram-only coverage if setup was declined. Do not restart bootstrap from this audit.' : ' State that the framework is unknown; continue the diagram and planned assertions. If proposing a new framework, settle that choice through Decision procedure in Test step 5. Reuse an exact prior approval; with no selection proposed, ask no framework question. Do not install a framework or write the proposed tests during this review.'}`);
+1 -3
View File
@@ -8,9 +8,7 @@ Every diff gets the Claude adversarial pass. Add Codex when its preflight is rea
```bash
DIFF_BASE=$(git merge-base origin/<base> HEAD)
DIFF_INS=$(git diff "$DIFF_BASE" --stat | tail -1 | grep -oE '[0-9]+ insertion' | grep -oE '[0-9]+' || echo "0")
DIFF_DEL=$(git diff "$DIFF_BASE" --stat | tail -1 | grep -oE '[0-9]+ deletion' | grep -oE '[0-9]+' || echo "0")
DIFF_TOTAL=$((DIFF_INS + DIFF_DEL))
DIFF_TOTAL=$(git diff "$DIFF_BASE" --numstat | awk '{n+=$1+$2} END {print n+0}')
echo "DIFF_SIZE: $DIFF_TOTAL"
```
+2
View File
@@ -276,6 +276,7 @@ STACK=""
[ -f requirements.txt ] || [ -f pyproject.toml ] && STACK="${STACK}python "
[ -f go.mod ] && STACK="${STACK}go "
[ -f Cargo.toml ] && STACK="${STACK}rust "
[ -f pubspec.yaml ] && STACK="${STACK}dart "
echo "STACK: ${STACK:-unknown}"
DIFF_BASE=$(git merge-base origin/<base> HEAD)
DIFF_LINES=$(git diff "$DIFF_BASE" --numstat | awk '{n+=$1+$2} END {print n+0}')
@@ -287,6 +288,7 @@ TEST_FW=""
{ [ -f spec/spec_helper.rb ] || [ -f .rspec ]; } && TEST_FW="rspec"
{ [ -f pytest.ini ] || [ -f conftest.py ]; } && TEST_FW="pytest"
[ -f go.mod ] && TEST_FW="go-test"
[ -f pubspec.yaml ] && TEST_FW="dart-test" && grep -qsE '^ +sdk: *.?flutter' pubspec.yaml && TEST_FW="flutter-test"
echo "TEST_FW: ${TEST_FW:-unknown}"
```
+5 -4
View File
@@ -48,20 +48,21 @@ Before analyzing coverage, detect the project's test framework:
```bash
setopt +o nomatch 2>/dev/null || true # zsh compat
# Detect project runtime (markers are evidence, not commands to run blind)
# Runtime markers: evidence, not commands to run
[ -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 go.mod ] && echo "RUNTIME:go"
[ -f Cargo.toml ] && echo "RUNTIME:rust"
[ -f pubspec.yaml ] && echo "RUNTIME:dart"
[ -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
# Test infra: configs, scripts, test files
ls jest.config.* vitest.config.* playwright.config.* cypress.config.* .rspec pytest.ini tox.ini phpunit.xml dart_test.yaml 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:/'
git ls-files | grep -cE '(^|/)(tests?|spec|__tests__)/|(^|/)tests?\.py$|(^|/)test_[^/]+\.py$|_test\.(go|py|rb|ts|js|exs|dart)$|\.(test|spec)\.[jt]sx?$|_spec\.rb$|Test\.(java|kt)$' | sed 's/^/TESTFILES:/'
```
3. **If no framework detected:** use the bootstrap decision already made in Step 4; report diagram-only coverage if setup was declined. Do not restart bootstrap from this audit.
+12 -9
View File
@@ -6,17 +6,18 @@
**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.
**Otherwise gather markers. Every marker below is EVIDENCE for the question you ask — never a command to run blind.** A marker names the ecosystem and the command to OFFER; it does not prove the command works. Never execute a candidate to "check" it: probing a runner the project never had fails loudly and teaches nothing.
```bash
setopt +o nomatch 2>/dev/null || true # zsh compat
# Definitive ecosystem markers (presence = ecosystem, NOT a command to run)
# Ecosystem markers
[ -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 go.mod ] && echo "RUNTIME:go"
[ -f Cargo.toml ] && echo "RUNTIME:rust"
[ -f pubspec.yaml ] && echo "RUNTIME:dart"
[ -f composer.json ] && echo "RUNTIME:php"
[ -f mix.exs ] && echo "RUNTIME:elixir"
[ -f pom.xml ] && echo "RUNTIME:jvm BUILD:maven"
@@ -24,14 +25,14 @@ setopt +o nomatch 2>/dev/null || true # zsh compat
# 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"
# 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
grep -qsE '^ +sdk: *.?flutter' pubspec.yaml && echo "FRAMEWORK:flutter"
# Test evidence: config files, declared scripts, AND test FILES (tested projects often lack a config)
ls jest.config.* vitest.config.* playwright.config.* .rspec pytest.ini tox.ini phpunit.xml* dart_test.yaml 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
git ls-files | grep -cE '(^|/)(tests?|spec|__tests__)/|(^|/)tests?\.py$|(^|/)test_[^/]+\.py$|_test\.(go|py|rb|ts|js|exs|dart)$|\.(test|spec)\.[jt]sx?$|_spec\.rb$|Test\.(java|kt)$' | sed 's/^/TESTFILES:/'
# Rust unit tests live in src/; 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"
@@ -45,6 +46,7 @@ Map the markers to the command you will OFFER — never to one you run on a gues
| `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` |
| `pubspec.yaml` | Dart / Flutter | `flutter test` (FRAMEWORK:flutter) or `dart test`; `fvm` prefix if `.fvmrc` |
| `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` |
@@ -53,11 +55,11 @@ Map the markers to the command you will OFFER — never to one you run on a gues
| `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.
**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, Dart, 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 Step 7. **Skip the rest of bootstrap.**
Absent config files and absent `tests/` directories are NOT evidence of "no tests": Django keeps tests in `<app>/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.
Absent config files and absent `tests/` directories are NOT evidence of "no tests": Django keeps tests in `<app>/tests.py`, Go in `*_test.go` beside the source, Rust in `#[test]` blocks inside `src/`.
**If BOOTSTRAP_DECLINED** appears:
- Step 5's explicit Add tests choice overrides that marker for this invocation only: continue to runtime detection and B2–B3, including framework approval.
@@ -103,6 +105,7 @@ If Aside is not installed or not running (`command -v aside` prints nothing, or
| Go | stdlib testing + testify | stdlib only |
| JVM (Maven/Gradle) | JUnit 5 + AssertJ | JUnit 5 only |
| Rust | cargo test (built-in) + mockall | — |
| Dart | test / flutter_test + mocktail | mockito |
| PHP | phpunit + mockery | pest |
| Elixir | ExUnit (built-in) + ex_machina | — |
+13
View File
@@ -87,6 +87,19 @@ describe('gstack-diff-scope', () => {
}
});
// A Dart change matched no category but its test file, so a Flutter widget PR
// set SCOPE_TESTS alone and skipped the design, performance and security passes.
test('detects Dart as both frontend and backend; pubspec.lock is config', () => {
const scope = runScope(createRepo(['lib/theme.dart', 'lib/widgets/drawer.dart', 'test/drawer_test.dart']));
expect(scope.SCOPE_FRONTEND).toBe('true');
expect(scope.SCOPE_BACKEND).toBe('true');
expect(scope.SCOPE_TESTS).toBe('true');
expect(runScope(createRepo(['pubspec.lock'])).SCOPE_CONFIG).toBe('true');
// Value: protects=a workspace package's pubspec.lock still reads as config; fails_when=the */pubspec.lock arm is dropped;
// why_new=only the root lock was checked; seam=none; no_claim=none beyond protects
expect(runScope(createRepo(['packages/cache/pubspec.lock'])).SCOPE_CONFIG).toBe('true');
});
test('detects test files', () => {
const dir = createRepo(['test/app.test.ts']);
const scope = runScope(dir);
+31 -31
View File
@@ -4,92 +4,92 @@
"pins": {
"agy/gstack-autoplan": 130339,
"agy/gstack-design-consultation": 108037,
"agy/gstack-design-review": 143386,
"agy/gstack-design-review": 143381,
"agy/gstack-land-and-deploy": 116578,
"agy/gstack-plan-design-review": 139980,
"agy/gstack-plan-devex-review": 135727,
"agy/gstack-plan-eng-review": 152669,
"agy/gstack-review": 124266,
"claude/design-review": 136826,
"agy/gstack-plan-eng-review": 152668,
"agy/gstack-review": 124086,
"claude/design-review": 136821,
"codex/gstack-autoplan": 130298,
"codex/gstack-design-consultation": 109239,
"codex/gstack-design-review": 144820,
"codex/gstack-design-review": 144815,
"codex/gstack-land-and-deploy": 121155,
"codex/gstack-office-hours": 102108,
"codex/gstack-plan-design-review": 142367,
"codex/gstack-plan-devex-review": 138550,
"codex/gstack-plan-eng-review": 155149,
"codex/gstack-review": 125071,
"codex/gstack-plan-eng-review": 155148,
"codex/gstack-review": 124891,
"copilot/gstack-autoplan": 131472,
"copilot/gstack-codex": 100103,
"copilot/gstack-design-consultation": 109389,
"copilot/gstack-design-review": 144691,
"copilot/gstack-design-review": 144686,
"copilot/gstack-land-and-deploy": 117848,
"copilot/gstack-office-hours": 100396,
"copilot/gstack-plan-design-review": 140851,
"copilot/gstack-plan-devex-review": 136879,
"copilot/gstack-plan-eng-review": 153394,
"copilot/gstack-review": 125537,
"copilot/gstack-plan-eng-review": 153393,
"copilot/gstack-review": 125357,
"cursor/gstack-autoplan": 129113,
"cursor/gstack-design-consultation": 107075,
"cursor/gstack-design-review": 142365,
"cursor/gstack-design-review": 142360,
"cursor/gstack-land-and-deploy": 115517,
"cursor/gstack-plan-design-review": 138935,
"cursor/gstack-plan-devex-review": 134558,
"cursor/gstack-plan-eng-review": 151523,
"cursor/gstack-review": 140637,
"cursor/gstack-plan-eng-review": 151522,
"cursor/gstack-review": 140618,
"factory/gstack-autoplan": 131397,
"factory/gstack-codex": 100523,
"factory/gstack-design-consultation": 108523,
"factory/gstack-design-review": 144155,
"factory/gstack-design-review": 144150,
"factory/gstack-land-and-deploy": 119700,
"factory/gstack-office-hours": 101275,
"factory/gstack-plan-design-review": 141716,
"factory/gstack-plan-devex-review": 137927,
"factory/gstack-plan-eng-review": 154637,
"factory/gstack-review": 144416,
"factory/gstack-plan-eng-review": 154636,
"factory/gstack-review": 144397,
"gbrain/gstack-autoplan": 129650,
"gbrain/gstack-design-review": 132733,
"gbrain/gstack-design-review": 132728,
"gbrain/gstack-land-and-deploy": 116039,
"gbrain/gstack-plan-design-review": 127855,
"gbrain/gstack-plan-devex-review": 115129,
"gbrain/gstack-plan-eng-review": 134288,
"gbrain/gstack-plan-eng-review": 134287,
"hermes/gstack-autoplan": 129740,
"hermes/gstack-design-review": 132840,
"hermes/gstack-design-review": 132835,
"hermes/gstack-land-and-deploy": 116141,
"hermes/gstack-plan-design-review": 127964,
"hermes/gstack-plan-devex-review": 115215,
"hermes/gstack-plan-eng-review": 134373,
"hermes/gstack-plan-eng-review": 134372,
"kiro/gstack-autoplan": 128875,
"kiro/gstack-design-consultation": 106917,
"kiro/gstack-design-review": 142179,
"kiro/gstack-design-review": 142174,
"kiro/gstack-land-and-deploy": 115407,
"kiro/gstack-plan-design-review": 138775,
"kiro/gstack-plan-devex-review": 134398,
"kiro/gstack-plan-eng-review": 151373,
"kiro/gstack-review": 140449,
"kiro/gstack-plan-eng-review": 151372,
"kiro/gstack-review": 140430,
"openclaw/gstack-autoplan": 129355,
"openclaw/gstack-design-review": 131538,
"openclaw/gstack-design-review": 131533,
"openclaw/gstack-land-and-deploy": 115627,
"openclaw/gstack-plan-design-review": 126706,
"openclaw/gstack-plan-devex-review": 113968,
"openclaw/gstack-plan-eng-review": 132685,
"openclaw/gstack-plan-eng-review": 132684,
"opencode/gstack-autoplan": 131475,
"opencode/gstack-design-consultation": 109210,
"opencode/gstack-design-review": 144563,
"opencode/gstack-design-review": 144558,
"opencode/gstack-land-and-deploy": 117625,
"opencode/gstack-office-hours": 100230,
"opencode/gstack-plan-design-review": 140705,
"opencode/gstack-plan-devex-review": 136779,
"opencode/gstack-plan-eng-review": 153275,
"opencode/gstack-review": 142872,
"opencode/gstack-plan-eng-review": 153274,
"opencode/gstack-review": 142853,
"slate/gstack-autoplan": 128994,
"slate/gstack-design-consultation": 106996,
"slate/gstack-design-review": 142272,
"slate/gstack-design-review": 142267,
"slate/gstack-land-and-deploy": 115462,
"slate/gstack-plan-design-review": 138855,
"slate/gstack-plan-devex-review": 134478,
"slate/gstack-plan-eng-review": 151448,
"slate/gstack-review": 140543
"slate/gstack-plan-eng-review": 151447,
"slate/gstack-review": 140524
}
}
+4 -1
View File
@@ -44,6 +44,9 @@ const SAMPLES: Array<[string, boolean]> = [
['components.md', false],
['public/Index.HTML', false], // bash globs are case-sensitive; the mirror must agree
['src/App.TSX', false],
['lib/widgets/bbtm_drawer/view/bbtm_drawer.dart', true],
['packages/cache/lib/cache.dart', true],
['test/ui/drawer_widget_test.dart', true],
];
describe('isFrontendPath', () => {
@@ -86,5 +89,5 @@ describe.skipIf(!POSIX)('parity with bin/gstack-diff-scope', () => {
} finally {
fs.rmSync(dir, { recursive: true, force: true });
}
});
}, 30_000); // one git commit + one bash run per sample: past bun's 5 s default
});
+6 -1
View File
@@ -29,7 +29,7 @@ let IMPECCABLE_HOME: string;
function git(cwd: string, ...args: string[]) {
const r = spawnSync('git', args, { cwd, encoding: 'utf-8', timeout: 30_000 });
if (r.status !== 0) throw new Error(`git ${args.join(' ')} failed: ${r.stderr}`);
if (r.status !== 0) throw new Error(`git ${args.join(' ')} failed: status=${r.status} signal=${r.signal} error=${r.error?.message ?? ''} stderr=${r.stderr ?? ''}`);
return r.stdout.trim();
}
@@ -907,6 +907,10 @@ describe('scan', () => {
fs.mkdirSync(path.join(REPO, 'styles'), { recursive: true });
fs.writeFileSync(path.join(REPO, 'styles', 'new.css'), 'body { color: red }\n'); // untracked frontend
fs.writeFileSync(path.join(REPO, 'notes.md'), 'x\n'); // untracked non-frontend
// Value: protects=a .dart file (frontend scope since lib/frontend-scope.ts counts it) never reaches the web-source engine;
// fails_when=the .dart exclusion in resolveTargets is dropped; why_new=no --changed case had a Dart file; seam=none;
// no_claim=the real impeccable engine (fake engine logs argv)
fs.writeFileSync(path.join(REPO, 'drawer.dart'), 'class Drawer {}\n'); // untracked Dart: frontend scope, not a scan target
const log = path.join(SANDBOX, 'argv4.log');
fs.rmSync(log, { force: true });
try {
@@ -918,6 +922,7 @@ describe('scan', () => {
} finally {
fs.rmSync(path.join(REPO, 'styles'), { recursive: true, force: true });
fs.rmSync(path.join(REPO, 'notes.md'), { force: true });
fs.rmSync(path.join(REPO, 'drawer.dart'), { force: true });
git(REPO, 'checkout', '-q', 'main');
git(REPO, 'branch', '-q', '-D', 'feature');
}
+28
View File
@@ -240,3 +240,31 @@ describe('outside reviewer runtime discovery in fresh shells', () => {
expect(fs.existsSync(path.join(f.repo, 'NEVER'))).toBe(false);
});
});
// Value: protects=the adversarial DIFF_SIZE sums insertions+deletions across files and ignores binary rows;
// fails_when=the numstat/awk sum reads the wrong field or a binary "-" row breaks it; why_new=no test ran the
// size block; seam=none; no_claim=the size thresholds that consume DIFF_SIZE
test('adversarial DIFF_SIZE counts changed lines from numstat', () => {
const ctx: TemplateContext = { host: 'claude', skillName: 'ship', tmplPath: 'ship/SKILL.md.tmpl', paths: HOST_PATHS.claude };
const fence = generateAdversarialStep(ctx).match(/```bash\n(DIFF_BASE=[\s\S]*?)\n```/);
expect(fence).not.toBeNull();
const repo = fs.mkdtempSync(path.join(TEMP, 'diff-size-'));
const git = (...args: string[]) => {
const r = spawnSync('git', ['-c', 'user.email=t@t', '-c', 'user.name=t', ...args], { cwd: repo, encoding: 'utf8', timeout: 30_000 });
expect(r.status).toBe(0);
};
git('init', '-q', '-b', 'main');
fs.writeFileSync(path.join(repo, 'a.txt'), 'one\ntwo\n');
git('add', '-A');
git('commit', '-qm', 'base');
git('update-ref', 'refs/remotes/origin/main', 'HEAD');
git('checkout', '-q', '-b', 'feature');
fs.writeFileSync(path.join(repo, 'a.txt'), 'one\nTWO\n'); // 1 insertion + 1 deletion
fs.writeFileSync(path.join(repo, 'b.txt'), 'x\ny\nz\n'); // 3 insertions
fs.writeFileSync(path.join(repo, 'c.bin'), Buffer.from([0, 1, 2, 0])); // binary: "-\t-" in numstat
git('add', '-A');
git('commit', '-qm', 'change');
const r = spawnSync('bash', ['-c', fence![1].replaceAll('<base>', 'main')], { cwd: repo, encoding: 'utf8', timeout: 30_000 });
expect(r.stderr).toBe('');
expect(r.stdout.trim()).toBe('DIFF_SIZE: 5');
});
+83 -10
View File
@@ -20,21 +20,23 @@ import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { generateTestBootstrap } from '../scripts/resolvers/testing';
import { generateTestBootstrap, generateTestCoverageAuditShip } from '../scripts/resolvers/testing';
import { generateReviewArmy } from '../scripts/resolvers/review-army';
import { HOST_PATHS } from '../scripts/resolvers/types';
const section = generateTestBootstrap({} as never);
/** The detection block the skill tells the agent to run (first bash fence). */
function detectionScript(): string {
const open = section.indexOf('```bash\n');
function detectionScript(from: string = section): string {
const open = from.indexOf('```bash\n');
expect(open).toBeGreaterThan(-1);
const start = open + '```bash\n'.length;
const end = section.indexOf('```', start);
return section.slice(start, end);
const end = from.indexOf('```', start);
return from.slice(start, end);
}
/** Run the detection block in a throwaway git repo laid out by `files`. */
function detect(files: Record<string, string>): string {
function detect(files: Record<string, string>, script: string = detectionScript()): string {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ship-detect-'));
try {
for (const [rel, body] of Object.entries(files)) {
@@ -42,15 +44,15 @@ function detect(files: Record<string, string>): string {
fs.mkdirSync(path.dirname(abs), { recursive: true });
fs.writeFileSync(abs, body);
}
const script = path.join(dir, '.detect.sh');
fs.writeFileSync(script, detectionScript());
const scriptPath = path.join(dir, '.detect.sh');
fs.writeFileSync(scriptPath, script);
const git = (...args: string[]) => execFileSync('git', args, { timeout: 30_000, cwd: dir });
git('init', '-q', '.');
git('add', '-A');
git('-c', 'user.email=t@t', '-c', 'user.name=t', 'commit', '-qm', 'fixture');
// The block's last line is a `[ -f marker ] && echo`, so a clean project
// exits 1 by design — read stdout, don't trust the status.
return execFileSync('bash', [script], { timeout: 30_000, cwd: dir, encoding: 'utf-8' });
return execFileSync('bash', [scriptPath], { timeout: 30_000, cwd: dir, encoding: 'utf-8' });
} catch (err: unknown) {
const e = err as { stdout?: string };
if (typeof e.stdout === 'string') return e.stdout;
@@ -93,6 +95,37 @@ describe('/ship Step 4 detection is multi-ecosystem', () => {
expect(rust).toContain('TESTS:rust in-source');
});
test('Dart and Flutter report their runtime, the Flutter SDK, and *_test.dart outside test/', () => {
const flutter = detect({
'pubspec.yaml': 'name: app\ndependencies:\n flutter:\n sdk: flutter\n',
'lib/src/theme_test.dart': 'void main() {}\n',
});
expect(flutter).toContain('RUNTIME:dart');
expect(flutter).toContain('FRAMEWORK:flutter');
expect(testFileCount(flutter)).toBe(1);
// A commented-out Flutter dependency is not Flutter.
const dart = detect({ 'pubspec.yaml': 'name: x\n# flutter:\n# sdk: flutter\n', 'lib/x.dart': 'int x = 1;\n' });
expect(dart).toContain('RUNTIME:dart');
expect(dart).not.toContain('FRAMEWORK:flutter');
expect(testFileCount(dart)).toBe(0);
// dart_test.yaml can move discovery off *_test.dart (paths/filename), so it is test evidence on its own.
const configured = detect({ 'pubspec.yaml': 'name: x\n', 'dart_test.yaml': 'paths: [checks]\n', 'checks/test_math.dart': 'void main() {}\n' });
expect(configured).toContain('dart_test.yaml');
});
// Value: protects=the Step 7 coverage audit sees a Dart project and its *_test.dart files, like bootstrap does;
// fails_when=the coverage-audit block drops RUNTIME:dart or `dart` from its _test regex; why_new=only the
// bootstrap block was run; seam=none; no_claim=none beyond protects
test('the Step 7 coverage-audit detection block agrees with bootstrap on Dart', () => {
const audit = generateTestCoverageAuditShip({ host: 'claude', skillName: 'ship', tmplPath: 'ship/SKILL.md.tmpl', paths: HOST_PATHS.claude } as never);
const block = audit.slice(audit.indexOf('Test Framework Detection'));
const out = detect({ 'pubspec.yaml': 'name: x\n', 'lib/src/theme_test.dart': 'void main() {}\n' }, detectionScript(block));
expect(out).toContain('RUNTIME:dart');
expect(testFileCount(out)).toBe(1);
});
test('a genuinely untested project reports no test evidence', () => {
const out = detect({ 'go.mod': 'module x\n', 'x.go': 'package x\n' });
expect(out).toContain('RUNTIME:go');
@@ -103,7 +136,7 @@ describe('/ship Step 4 detection is multi-ecosystem', () => {
test('every ecosystem marker maps to a command to OFFER, not to run blind', () => {
expect(section).toContain('never a command to run blind');
for (const marker of ['manage.py', 'go.mod', 'Cargo.toml', 'pom.xml',
'build.gradle', 'mix.exs', 'composer.json', 'Gemfile', 'pytest.ini']) {
'build.gradle', 'mix.exs', 'composer.json', 'Gemfile', 'pytest.ini', 'pubspec.yaml']) {
expect(section).toContain(marker);
}
// The ask-and-persist contract, not a hardcoded project command.
@@ -111,3 +144,43 @@ describe('/ship Step 4 detection is multi-ecosystem', () => {
expect(section).toContain('persist the answer to CLAUDE.md');
});
});
// Value: protects=review-army STACK/TEST_FW name Dart and tell flutter_test from dart test; fails_when=the pubspec.yaml lines
// are dropped or the Flutter grep matches any `sdk:` (environment.sdk), a comment, or misses a quoted value; why_new=no free test runs the review-army
// detection lines; seam=none; no_claim=specialist selection from these values (only the emitted lines run)
describe('review-army stack detection names Dart and Flutter', () => {
const block = generateReviewArmy({ host: 'claude', skillName: 'ship', tmplPath: 'ship/SKILL.md.tmpl', paths: HOST_PATHS.claude });
const lines = (from: string, to: string): string => {
const start = block.indexOf(from);
const end = block.indexOf(to, start);
expect(start).toBeGreaterThan(-1);
expect(end).toBeGreaterThan(start);
return block.slice(start, block.indexOf('\n', end));
};
const script = `${lines('STACK=""', 'echo "STACK:')}\n${lines('TEST_FW=""', 'echo "TEST_FW:')}\n`;
function stack(files: Record<string, string>): string {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'review-army-stack-'));
try {
for (const [rel, body] of Object.entries(files)) fs.writeFileSync(path.join(dir, rel), body);
return execFileSync('bash', ['-c', script], { timeout: 30_000, cwd: dir, encoding: 'utf-8' });
} finally {
fs.rmSync(dir, { recursive: true, force: true });
}
}
test('Flutter app, pure Dart package, and a Dart SDK constraint that is not Flutter', () => {
// Quoted `sdk: "flutter"` is the same YAML value.
const flutter = stack({ 'pubspec.yaml': 'name: app\ndependencies:\n flutter:\n sdk: "flutter"\n' });
expect(flutter).toContain('STACK: dart');
expect(flutter).toContain('TEST_FW: flutter-test');
const dart = stack({ 'pubspec.yaml': 'name: x\nenvironment:\n sdk: ">=3.0.0 <4.0.0"\n# sdk: flutter\n' });
expect(dart).toContain('STACK: dart');
expect(dart).toContain('TEST_FW: dart-test');
const none = stack({ 'README.md': 'x\n' });
expect(none).toContain('STACK: unknown');
expect(none).toContain('TEST_FW: unknown');
});
});