Files
claude-engineering-plugin/docs/plans/2026-04-16-001-fix-ce-polish-beta-detection-gaps-plan.md
2026-04-16 17:55:10 -05:00

457 lines
41 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: fix: Close ce-polish-beta detection gaps from PR #568 feedback
type: fix
status: active
date: 2026-04-16
---
# fix: Close ce-polish-beta detection gaps from PR #568 feedback
## Overview
Address four concrete detection/resolution gaps in `ce-polish-beta` raised by @tmchow on EveryInc/compound-engineering-plugin#568:
1. Framework coverage — Nuxt, SvelteKit, Remix, Astro fall through to `unknown` (the commenter calls them "table stakes alongside Next and Vite")
2. Monorepo blind spot — `detect-project-type.sh` only inspects the repo root, so a Turborepo with `apps/web/next.config.js` returns `unknown`
3. Package-manager detection is documented in prose but not implemented; Next/Vite stubs silently write `npm run dev` on pnpm/yarn/bun projects
4. Port cascade is lossy — `.env` reader doesn't strip quotes or trailing comments, `AGENTS.md`/`CLAUDE.md` grep hits unrelated doc references, no probe of `next.config.*` / `vite.config.*` / `config/puma.rb` / `docker-compose.yml`
All four are detection/resolution bugs in an already-shipped beta skill (`disable-model-invocation: true`, so no auto-trigger regression risk). Fix scope is the skill's own `scripts/` and `references/` trees plus the Phase 3 wiring in `SKILL.md`.
## Problem Frame
Polish's dev-server lifecycle (Phase 3 in SKILL.md) has three resolution jobs:
- **What project type is this?** → `scripts/detect-project-type.sh`
- **How do I start it?** → per-type recipe in `references/dev-server-<type>.md`, substituted into a `launch.json` stub
- **What port will it bind to?** → inline cascade documented in `references/dev-server-detection.md`
All three jobs currently fail for common-but-unhandled shapes (monorepos, Nuxt/Astro, pnpm-only repos, quoted `.env` values). Users hit these gaps the first time they run polish on anything outside the four project types the skill was bootstrapped with (rails, next, vite, procfile). The fallback — "ask the user to author `.claude/launch.json`" — works but pushes onto the user a discovery problem the skill should do itself.
Feedback is the first real contact the skill has had with a reviewer outside the original plan, and it lines up with hazards already flagged in `references/dev-server-vite.md` ("SvelteKit, SolidStart, Qwik City, and Astro all use Vite… Different default ports apply") and `references/dev-server-next.md` ("Monorepo roots: users should set `cwd`… to the specific Next app"). The skill knew these were gaps and punted — this plan closes the punt.
## Requirements Trace
- **R1.** Nuxt, SvelteKit, Astro, and Remix are recognized first-class project types (no longer fall through to `unknown`).
- **R2.** `detect-project-type.sh` finds a framework config inside a monorepo workspace (up to a bounded depth) and returns a type + relative `cwd`, so the stub-writer can populate `cwd` in `launch.json` without user intervention.
- **R3.** Next and Vite stubs use the package manager indicated by the lockfile (`pnpm` / `yarn` / `bun` / `npm`) instead of hard-coding `npm`.
- **R4.** Port resolution prefers authoritative config files (framework config, `config/puma.rb`, `Procfile.dev`, `docker-compose.yml`) over prose references. `.env` parsing correctly strips surrounding quotes and trailing `# comment`. The noisy `AGENTS.md`/`CLAUDE.md` grep is removed.
- **R5.** Existing users are not regressed. Repos that previously detected correctly continue to detect the same type; repos with `.claude/launch.json` are unaffected (launch.json still wins).
- **R6.** Each new or modified script has unit-test coverage in `tests/skills/` mirroring the existing `ce-polish-beta-dev-server.test.ts` harness (tmp git repo, Bun.spawn, exit-code + stdout assertions).
## Scope Boundaries
- **Not** adding Python (Django, Flask, FastAPI), Go, Elixir/Phoenix, Deno/Fresh, Angular, Gatsby, Expo, Electron, Tauri, Storybook, or Ruby non-Rails (Sinatra, Hanami). Trevor listed these as gaps; they each need their own recipe file and dev-server conventions, and together they would roughly double the skill's surface area. Defer to a follow-up plan.
- **Not** changing `.claude/launch.json` priority — launch.json always wins over auto-detect. This plan only improves what auto-detect does when launch.json is absent.
- **Not** rewriting the IDE handoff, kill-by-port, or reachability probe in Phase 3.5/3.6. Those are unaffected.
- **Not** changing headless-mode semantics. All new scripts are probes; they don't mutate state, so headless rules ("never write .claude/launch.json, never kill without token") are preserved.
- **Not** adding a framework config parser beyond a conservative regex. Arbitrary JS/TS config files can set `port` via computed expressions the regex won't catch; when the probe misses, the cascade falls through to framework defaults. Document this as best-effort, not authoritative.
- **Not** bumping plugin version, marketplace version, or writing a release entry. Per repo `AGENTS.md`, release-please owns that.
## Context & Research
### Relevant Code and Patterns
- `plugins/compound-engineering/skills/ce-polish-beta/scripts/detect-project-type.sh` — current root-only classifier with precedence rules (rails beats procfile, `multiple` for real disambiguation)
- `plugins/compound-engineering/skills/ce-polish-beta/scripts/read-launch-json.sh` — existing script that emits sentinel outputs (`__NO_LAUNCH_JSON__`, `__INVALID_LAUNCH_JSON__`, `__MISSING_CONFIGURATIONS__`, `__CONFIG_NOT_FOUND__`). The sentinel pattern is the convention new scripts should follow for signaling "no match, fall through"
- `plugins/compound-engineering/skills/ce-polish-beta/scripts/parse-checklist.sh` — pattern for set-unsafe `set -u`, bash regex (`[[ =~ ]]`), and awk/jq composition within a single script. New scripts should match this style (no `set -euo pipefail`; the existing scripts use `set -u` only, by convention)
- `plugins/compound-engineering/skills/ce-polish-beta/references/dev-server-<rails|next|vite|procfile>.md` — per-type recipe shape: Signature, Start command, Port, Stub generation, Common gotchas
- `plugins/compound-engineering/skills/ce-polish-beta/references/launch-json-schema.md` — stub templates grouped by project type; the stub-writer block to parameterize
- `tests/skills/ce-polish-beta-dev-server.test.ts` — test harness pattern: tmp git repo, touch signature files, invoke script via `Bun.spawn`, assert `exitCode` + `stdout.trim()`. All new scripts follow this shape.
- `plugins/compound-engineering/skills/ce-polish-beta/SKILL.md` Phase 3.2 (lines 272-291) — project-type routing table; the surface that needs extending for new types and the `<type>@<cwd>` return variant
- `plugins/compound-engineering/skills/ce-polish-beta/SKILL.md` Phase 3.3 (lines 293-303) — stub-writer; where package-manager substitution and `cwd` population land
### Institutional Learnings
None directly applicable; this work extends patterns already proven in the same skill.
### Cross-Repo Reference (informational only)
`plugins/compound-engineering/skills/test-browser/SKILL.md` has an inline port cascade that polish's `dev-server-detection.md` is a copy of (per the self-contained-skill rule). This plan does not modify `test-browser` — the two cascades stay independent by design. Note for maintainers: if test-browser adopts a parallel resolve-port script later, the two skills will need the standard manual-sync note updated.
## Key Technical Decisions
- **Decision: detect-project-type.sh returns `<type>` at root and `<type>@<cwd>` for monorepo hits, never just `<cwd>`.** Rationale: keeps the existing single-token protocol intact for the 90% root-detection case; downstream readers split on `@` when present. `@` is chosen over `:` because `:` is reserved for the outer multi-hit separator (see below). Alternative considered: return structured JSON. Rejected because every other script in `scripts/` returns plain-text tokens and consumers use `case`/`awk` on them, and JSON would force `jq` onto a detector that today only uses bash builtins.
- **Decision: Output grammar is `<type>` or `<type>@<cwd>` for single hits, `multiple` or `multiple:<type>@<cwd>,<type>@<cwd>,...` for multi-hits.** The four concrete shapes are:
- `next` (single hit at root)
- `next@apps/web` (single hit in monorepo)
- `multiple` (multiple signatures at root — existing behavior, unchanged)
- `multiple:next@apps/web,rails@apps/api` (multiple hits across monorepo workspaces, always emitted as `type@path` pairs even when types are the same)
Rationale: `:` is the outer multi-hit delimiter and `@` is the inner type-path delimiter, making the grammar unambiguous under naive `awk -F:` or bash parameter expansion. Document this explicitly in the script header comment so callers cannot misread it.
- **Decision: New scripts accept an optional path as a positional argument, not `--cwd`.** Rationale: every existing script in `scripts/` uses positional args (`parse-checklist.sh <path>`, `classify-oversized.sh <path> <path>`) or derives cwd from `git rev-parse --show-toplevel`. Flag-parsing would be a new convention. Follow the existing pattern: optional positional path defaults to `git rev-parse --show-toplevel`.
- **Decision: Expected-no-result sentinels exit 0, not 1.** Rationale: the existing convention in `read-launch-json.sh` (header comment on lines 20-21 of that file) reserves non-zero exit for operational failure only (missing `jq`, no git root). `__NO_PACKAGE_JSON__` and similar sentinels exit 0 with the sentinel on stdout; callers pattern-match on stdout, not exit code.
- **Decision: No provenance output on stderr.** Rationale: stderr across all existing scripts is reserved for `ERROR: ...` messages only. Provenance ("resolved_from: framework_config") would break that convention. `resolve-port.sh` emits a single-line integer on stdout, matching the simplicity of existing scripts. If future debugging surfaces real demand for provenance, add a second script or a `--verbose` mode in a follow-up — not speculatively.
- **Decision: Monorepo probe has a depth cap of 3 and walks only if root detection returned `unknown`.** Rationale: depth 3 covers the common layouts (`apps/web/next.config.js`, `packages/frontend/vite.config.ts`, `services/api/next.config.js`). Running unconditionally would slow the common case and risk false positives when the root is a known type with example configs nested elsewhere (fixtures, templates). Depth 3 is a hard cap because deeper nesting usually means the user already needs to author `launch.json`.
- **Decision: Exclude `node_modules/`, `.git/`, `vendor/`, `dist/`, `build/`, `coverage/`, `.next/`, `.nuxt/`, `.svelte-kit/`, `.turbo/`, `tmp/`, `fixtures/` from the monorepo probe.** Rationale: these directories ship config files as fixtures or build output that the user doesn't own. Without exclusion, a Rails app with `node_modules/next/.../examples/` would register as Next, and a monorepo with test fixtures would surface false positives.
- **Decision: `resolve-package-manager.sh` returns one token (`npm` / `pnpm` / `yarn` / `bun`) plus the start command (stdout line 1 and line 2 respectively) so stub-writer substitution is deterministic.** Rationale: `pnpm dev` and `bun run dev` use different argv shapes. A single-token return would force the consumer to maintain a lookup table; emitting both the binary and the canonical args keeps all PM-specific knowledge in one place (the resolver).
- **Decision: `resolve-port.sh` replaces the inline `dev-server-detection.md` cascade.** Rationale: the cascade lives in skill prose and has silently-buggy shell (unstripped quotes, noisy grep). Lifting it into a tested script with the sentinel-output convention makes the behavior assertable and fixes the bugs at the same site. `dev-server-detection.md` becomes a thin pointer to the script with the framework-default table retained.
- **Decision: Port cascade probes authoritative config files first, `.env*` second, default last.** Rationale: Trevor's core complaint is that the current cascade prefers *prose* (AGENTS.md) over *config* (next.config.js, config/puma.rb). Flipping that ordering restores "the code is the source of truth."
- **Decision: Drop the `AGENTS.md` / `CLAUDE.md` grep entirely.** Rationale: users who need to override have the explicit `--port` / `port:` CLI token and the `.claude/launch.json` escape hatch. Grepping instruction files for port numbers catches unrelated mentions ("connects to Stripe on port 8443", "example: localhost:3000") far more often than it captures a real override.
- **Decision: Framework config probes use a conservative regex and treat misses as "no pin, fall through".** Rationale: parsing arbitrary JS/TS reliably requires a JS runtime, which polish doesn't ship with. A regex that catches `port: 3000`, `port: "3000"`, and `server: { port: 3000 }` literals covers the common patterns. Missed ports fall through to framework default — same behavior as today, just with more chances to catch an explicit value along the way.
## Open Questions
### Resolved During Planning
- **Should Remix get a dedicated signature or route through Vite?** Resolved: both. Classic Remix ships `remix.config.js` without Vite; Remix 2.x+ ships `vite.config.ts`. Classic pattern gets its own signature in the detector so it resolves without ambiguity; new Remix continues to resolve as `vite` (the existing Vite recipe already documents SvelteKit/Astro/etc. as framework-on-Vite). The `remix` recipe notes both paths.
- **Should the monorepo probe return all matches or just one?** Resolved: return one if there's a single match, `multiple` with `<type>@<path>` pairs if several. Multiple matches at depth ≤3 is the genuine disambiguation case the existing `multiple` sentinel was designed for; the new output is `multiple:next@apps/web,next@apps/admin` so the interactive prompt in Phase 3.2 can list the options.
- **Where does SKILL.md document the new `<type>@<cwd>` format?** Resolved: extend the existing Phase 3.2 routing table with a "Paths with `@<cwd>` suffix" paragraph and update Phase 3.3 to substitute `cwd` when present. No new top-level section.
- **Does the port resolver need to parse `docker-compose.yml`?** Resolved: yes, but lightly — grep for `- "<port>:<port>"` under a `ports:` key on the service named `web` / `app` / `frontend`. Full YAML parsing is out of scope; a line-anchored regex catches the common compose shape and misses gracefully on exotic configs.
### Deferred to Implementation
- **Exact regex for framework config port probes.** Start with `port:\s*[0-9]+` and `port:\s*["']?[0-9]+["']?`, tighten if tests surface false positives. Unit 4 owns this.
- **Whether `pnpm dev` should be `pnpm dev` or `pnpm run dev`.** Both work; pick whichever is idiomatic per the current pnpm docs at the time of implementation and pin it in the resolver's lookup table.
- **Whether to probe `bun.lock` ahead of `bun.lockb`.** Bun recently added a text lockfile format (`bun.lock`) alongside the binary (`bun.lockb`); priority likely doesn't matter (only one will be present) but the resolver should match whichever is there.
## Implementation Units
- [x] **Unit 1: Add first-class recipes for Nuxt, Astro, Remix, SvelteKit**
**Goal:** Give the four "table stakes" JS frontend frameworks their own reference recipes with correct ports, start commands, and stub templates, so they stop falling through to `unknown`.
**Requirements:** R1, R6
**Dependencies:** None (recipe files are additive; they don't activate until Unit 2 extends the detector)
**Files:**
- Create: `plugins/compound-engineering/skills/ce-polish-beta/references/dev-server-nuxt.md`
- Create: `plugins/compound-engineering/skills/ce-polish-beta/references/dev-server-astro.md`
- Create: `plugins/compound-engineering/skills/ce-polish-beta/references/dev-server-remix.md`
- Create: `plugins/compound-engineering/skills/ce-polish-beta/references/dev-server-sveltekit.md`
- Modify: `plugins/compound-engineering/skills/ce-polish-beta/references/launch-json-schema.md` (add 4 stub templates)
**Approach:**
- Mirror the structure of `dev-server-next.md` exactly: Signature / Start command / Port / Stub generation / Common gotchas
- Defaults per the current framework docs: Nuxt port 3000, Astro port 4321, Remix port 3000 (classic) or 5173 (Vite), SvelteKit port 5173
- Each recipe's "Common gotchas" section notes interactions users will actually hit: Nuxt's Nitro, Astro's SSR vs SSG dev behavior, Remix's classic-vs-Vite fork, SvelteKit's adapter-free dev mode
- Stub templates in `launch-json-schema.md` match the existing Next/Vite/Rails/Procfile pattern
**Patterns to follow:**
- `plugins/compound-engineering/skills/ce-polish-beta/references/dev-server-next.md` for overall shape
- `plugins/compound-engineering/skills/ce-polish-beta/references/dev-server-vite.md` for framework-on-Vite notes (relevant to SvelteKit and new Remix)
**Test scenarios:** Test expectation: none — reference markdown is consumed by the model, not asserted. Unit 5's integration test covers that these recipes are selected correctly when their respective signatures are present.
**Verification:**
- Four new reference files exist with all five required sections
- `launch-json-schema.md` has stub templates for all four new types
- A reader landing on a new recipe can answer "what command do I run, at what port, with what launch.json stub?" without leaving the file
- [x] **Unit 2: Extend detect-project-type.sh with new signatures and monorepo probe**
**Goal:** The detector recognizes Nuxt/Astro/Remix/SvelteKit at the repo root and descends up to depth 3 into workspaces when root detection returns `unknown`, emitting `<type>` or `<type>@<cwd>` as appropriate.
**Requirements:** R1, R2, R5
**Dependencies:** Unit 1 (new types must have recipes before the detector returns them, so Phase 3.2 routing in Unit 5 doesn't dead-end)
**Files:**
- Modify: `plugins/compound-engineering/skills/ce-polish-beta/scripts/detect-project-type.sh`
- Create: `tests/skills/ce-polish-beta-project-type.test.ts`
**Approach:**
- Keep the existing root-scan precedence block intact (rails beats procfile, single-match returns `<type>`)
- Add signature checks for `nuxt.config.{js,mjs,ts}`, `astro.config.{js,mjs,ts}`, `remix.config.{js,ts}`, and `svelte.config.{js,mjs,ts}` at root
- When the root-scan yields zero matches, run a shallow `find` with `-maxdepth 3` excluding `node_modules`, `.git`, `vendor`, `dist`, `build`, `coverage`, `.next`, `.nuxt`, `.svelte-kit`, `.turbo`, `tmp`, `fixtures` looking for any supported signature filename
- Collect hits as `(type, relative-dir)` pairs. Deduplicate on the pair
- Single hit → emit `<type>@<cwd>` (or bare `<type>` when the hit is `.`)
- Multiple hits → emit `multiple:<type1>@<cwd1>,<type2>@<cwd2>,...` (always include the type prefix so the grammar is unambiguous under naive `awk -F:` on the outer separator)
- Zero monorepo hits → emit `unknown` unchanged
- **Header comment requirements:** document the output grammar explicitly (the four concrete shapes: `<type>` / `<type>@<cwd>` / `multiple` / `multiple:<type>@<cwd>,...`), the depth cap of 3 with its rationale, and the exclusion list. Callers should not have to reverse-engineer the grammar from examples
**Execution note:** Test-first — add the new test file with scenarios for each new signature, monorepo single-hit, monorepo multi-hit, exclusion of `node_modules`, and the unchanged-root-detection regression cases. Run the suite red, then modify the detector to go green. This script is load-bearing for dev-server startup and has no production telemetry; tests are the only safety net.
**Patterns to follow:**
- Existing `detect-project-type.sh` precedence block (rails-before-procfile)
- `tests/skills/ce-polish-beta-dev-server.test.ts` for test harness shape
**Test scenarios:**
- Happy path: `nuxt.config.ts` at root → `nuxt`
- Happy path: `astro.config.mjs` at root → `astro`
- Happy path: `remix.config.js` at root → `remix`
- Happy path: `svelte.config.js` at root → `sveltekit`
- Happy path: `apps/web/next.config.js` in Turborepo layout → `next@apps/web`
- Happy path: `packages/frontend/vite.config.ts` in pnpm-workspace layout → `vite@packages/frontend`
- Edge case: `apps/web/next.config.js` and `apps/admin/next.config.js``multiple:next@apps/web,next@apps/admin`
- Edge case: `apps/web/next.config.js` and `apps/api/Gemfile+bin/dev``multiple:next@apps/web,rails@apps/api`
- Edge case: signature inside `node_modules/next/examples/...` → ignored (root returns `unknown`)
- Edge case: signature at depth 4 (`projects/app/web/client/next.config.js`) → ignored
- Edge case: signature alongside `bin/dev`+`Gemfile` at root → returns `rails` (root wins, no probe runs)
- Regression: existing 4-type root detection unchanged when signatures present at root
- Regression: `Procfile.dev` + `bin/dev` + `Gemfile` → still returns `rails`, not `multiple`
**Verification:**
- All 12 test scenarios pass
- `bash scripts/detect-project-type.sh` run in a real Turborepo returns `next@apps/web` (or whichever app path matches)
- Run in the plugin's own repo root still returns the existing detection (or `unknown`, matching prior behavior)
- [x] **Unit 3: Package-manager resolver script**
**Goal:** A new `resolve-package-manager.sh` emits the project's package manager (`npm` / `pnpm` / `yarn` / `bun`) plus the canonical dev-server argv, so the stub-writer can substitute both without in-agent judgment.
**Requirements:** R3, R6
**Dependencies:** None
**Files:**
- Create: `plugins/compound-engineering/skills/ce-polish-beta/scripts/resolve-package-manager.sh`
- Create: `tests/skills/ce-polish-beta-package-manager.test.ts`
**Approach:**
- Accept an optional path as a positional argument (first positional); default to repo root via `git rev-parse --show-toplevel` when omitted
- In the resolved path, check for lockfiles in priority order: `pnpm-lock.yaml``yarn.lock``bun.lockb` / `bun.lock``package-lock.json`
- Emit two lines on stdout: line 1 = token (`npm` | `pnpm` | `yarn` | `bun`), line 2 = canonical command tail as a space-separated argv (e.g., `run dev` for npm/bun, `dev` for pnpm/yarn)
- Fall through to `npm` + `run dev` only when a `package.json` is present and no lockfile matches (matches prior hardcoded behavior, so no regression for vanilla projects). If the path is a valid directory but contains no `package.json`, do not fall through to `npm` — emit the sentinel instead (see next bullet), so callers can distinguish "JavaScript project with no lockfile" from "not a JavaScript project at all"
- If the path is a valid directory but contains no `package.json`, emit sentinel `__NO_PACKAGE_JSON__` on stdout and exit 0 (expected-no-match, matching `read-launch-json.sh` sentinel convention — callers pattern-match on stdout, not exit code)
- When both `bun.lockb` (binary) and `bun.lock` (text) are present in the same directory, prefer `bun.lock` (text). Rationale: Bun's text lockfile is the newer, canonical format; the binary format is a legacy variant. Only one will normally be present, but the resolver must deterministically pick one when both exist
- If the path itself does not exist or is not a directory, emit `ERROR:` on stderr and exit 1 (operational failure, distinct from expected-no-match)
- **Header comment requirements:** document the two-line stdout grammar (line 1 = binary, line 2 = argv tail), the lockfile priority order and why, and the sentinel-vs-error exit-code split
**Patterns to follow:**
- `plugins/compound-engineering/skills/ce-polish-beta/scripts/read-launch-json.sh` for sentinel outputs and exit codes
- Existing `detect-project-type.sh` for simple lockfile-presence checks
**Test scenarios:**
- Happy path: `pnpm-lock.yaml` present → stdout: `pnpm\ndev`
- Happy path: `yarn.lock` present → stdout: `yarn\ndev`
- Happy path: `bun.lockb` present → stdout: `bun\nrun dev`
- Happy path: `bun.lock` (text format) present → stdout: `bun\nrun dev`
- Happy path: `package-lock.json` present → stdout: `npm\nrun dev`
- Happy path: no lockfile, `package.json` present → stdout: `npm\nrun dev` (safe default)
- Edge case: both `pnpm-lock.yaml` and `yarn.lock` present → stdout: `pnpm\ndev` (priority order wins)
- Edge case: positional path pointing to `apps/web` — reads lockfile from subdir, not repo root
- Edge case: positional path to a directory without `package.json` → stdout `__NO_PACKAGE_JSON__`, exit 0 (expected-no-match sentinel)
- Edge case: no positional arg, not in a git repo → stderr `ERROR:` + exit 1 (operational failure)
- Edge case: positional path but directory doesn't exist → stderr `ERROR:` + exit 1 (operational failure)
**Verification:**
- All test scenarios pass
- Running from a real pnpm repo returns `pnpm\ndev`
- Running from a real npm repo returns `npm\nrun dev`
- [x] **Unit 4: Port resolver script with authoritative config probes**
**Goal:** A new `resolve-port.sh` probes config files in priority order (framework config → `config/puma.rb``Procfile.dev``docker-compose.yml``package.json` scripts → `.env*` → default), correctly parses `.env` values (stripping quotes and `# comment`), and drops the `AGENTS.md`/`CLAUDE.md` grep.
**Requirements:** R4, R6
**Dependencies:** None
**Files:**
- Create: `plugins/compound-engineering/skills/ce-polish-beta/scripts/resolve-port.sh`
- Create: `tests/skills/ce-polish-beta-resolve-port.test.ts`
- Modify: `plugins/compound-engineering/skills/ce-polish-beta/references/dev-server-detection.md`
**Approach:**
- Accept optional positional path as the first positional argument (defaults to `git rev-parse --show-toplevel` when omitted) — consistent with `parse-checklist.sh` and the Unit 3 resolver
- Accept optional `--type <rails|next|vite|nuxt|astro|remix|sveltekit|procfile>` flag to scope which probes run (e.g., skip `config/puma.rb` for Next). Type is a classification, not a path, so the flag form is appropriate and distinguishable from the positional path
- Accept optional `--port <n>` flag as an explicit override (emit immediately when present, before any probing)
- Probe order (first hit wins):
1. Explicit `--port` flag
2. Framework config: `next.config.*` / `vite.config.*` / `nuxt.config.*` / `astro.config.*` — conservative regex for `port:\s*["']?[0-9]+["']?` or `server.port\s*=\s*[0-9]+`. Numeric literals only; reject matches where the value is a variable reference (e.g., `process.env.PORT`, `getPort()`) so we do not emit a misleading default
3. Rails: `config/puma.rb` `port\s+[0-9]+`
4. Procfile: `Procfile.dev` `web:` line scanned for `-p <n>` / `--port <n>`
5. `docker-compose.yml`: in service named `web` / `app` / `frontend`, the first `"<n>:<n>"` line under `ports:`
6. `package.json` `dev`/`start` script for `--port <n>` / `-p <n>`
7. `.env*` files: check in override order **`.env.local``.env.development``.env`** (first hit wins, matching the convention most JS frameworks use where `.env.local` overrides `.env.development` which overrides `.env`). Parse `PORT=<n>`, stripping surrounding `"` or `'` and truncating at `#` (after trimming whitespace)
8. Framework default (emitted from a lookup table: rails/next/nuxt/remix=3000, vite/sveltekit=5173, astro=4321, procfile=3000, unknown=3000)
- Emit the resolved port as a single line on stdout. Do **not** emit provenance — stderr is reserved for `ERROR:` messages, matching the existing convention in `read-launch-json.sh` and `parse-checklist.sh`. If future debugging demand surfaces, add a `--verbose` mode in a follow-up rather than speculatively
- Rewrite `dev-server-detection.md`: the inline bash cascade is removed; the file becomes a navigable pointer ("Port resolution runs via `scripts/resolve-port.sh`") plus the framework-default table and probe-order rationale. Include an explicit **sync-note block** listing the three intentional divergences from `test-browser`'s inline cascade: (a) quote stripping on `.env` values, (b) comment stripping on `.env` values, (c) removal of the `AGENTS.md`/`CLAUDE.md` grep. The block tells a future maintainer of either skill exactly what not to "fix" back to symmetry
- **Header comment requirements:** document the probe-order rationale (config-before-prose), the `.env` parsing contract (quote + comment stripping), and the reason `AGENTS.md`/`CLAUDE.md` grepping is deliberately omitted
**Execution note:** Test-first — `.env` parsing bugs are the whole point. Write cases for quoted, single-quoted, comment-trailed, whitespace-padded, and multi-line forms first. Implement against those cases.
**Patterns to follow:**
- Existing cascade in `references/dev-server-detection.md` for probe order (improved, not replaced wholesale)
- `scripts/parse-checklist.sh` for bash regex patterns and awk/sed composition
- `scripts/read-launch-json.sh` for sentinel conventions and stderr-for-diagnostics
**Test scenarios:**
- Happy path: `--port 8080` explicit → `8080`
- Happy path: `next.config.js` with `port: 4000``4000`
- Happy path: `next.config.ts` with `server: { port: 4000 }``4000`
- Happy path: `config/puma.rb` with `port 3001``3001` (rails type)
- Happy path: `Procfile.dev` `web: bundle exec puma -p 4567``4567`
- Happy path: `docker-compose.yml` with `web:\n ports:\n - "9000:9000"``9000`
- Happy path: `package.json` `"dev": "next dev --port 4000"``4000`
- Edge case: `.env` `PORT=3001``3001`
- Edge case: `.env` `PORT="3001"``3001` (quotes stripped)
- Edge case: `.env` `PORT='3001'``3001` (single quotes stripped)
- Edge case: `.env` `PORT=3001 # dev only``3001` (comment stripped)
- Edge case: `.env` `PORT="3001" # quoted+commented``3001`
- Edge case: `.env` ` PORT = 3001 ``3001` (whitespace tolerated)
- Edge case: `.env.local` `PORT=4000` + `.env` `PORT=3000` both present → `4000` (`.env.local` precedence)
- Edge case: `.env.development` `PORT=4000` + `.env` `PORT=3000` both present → `4000` (`.env.development` precedence)
- Edge case: `.env.local` `PORT=4000` + `.env.development` `PORT=5000` both present → `4000` (`.env.local` beats `.env.development`)
- Edge case: multiple probes hit — framework config wins over `.env` (priority order)
- Edge case: no probe matches, `--type next``3000` (default)
- Edge case: no probe matches, `--type vite``5173`
- Edge case: no probe matches, `--type astro``4321`
- Edge case: no probe matches, no `--type``3000` (unknown default)
- Error path: malformed `docker-compose.yml` — probe misses, falls through (no crash)
- Error path: `next.config.js` with computed port (`port: getPort()`) — regex misses, falls through
- Error path: `next.config.js` with `port: process.env.PORT || 3000` — probe rejects the variable reference and falls through to `.env` / default (does not emit `3000` as if it were a framework-config hit)
- Error path: positional path does not exist → stderr `ERROR:` + exit 1 (operational failure, not a fall-through)
- Regression: `AGENTS.md` mentioning port `8443` in prose — ignored (grep removed)
- Regression: `CLAUDE.md` mentioning `localhost:3000` in examples — ignored
**Verification:**
- All 20+ test scenarios pass
- Running in the plugin's own repo root returns `3000` (default, since no framework config)
- Running against a synthetic Rails repo with `config/puma.rb port 3001` returns `3001`
- `dev-server-detection.md` no longer contains inline shell; it describes the probe order and framework-default table
- [x] **Unit 5: Wire new scripts and signatures into SKILL.md Phase 3**
**Goal:** SKILL.md Phase 3.2 routes the four new types and handles the `<type>@<cwd>` format; Phase 3.3 substitutes package-manager + cwd into stubs; port resolution calls `resolve-port.sh` instead of the inline cascade.
**Requirements:** R1, R2, R3, R4, R5
**Dependencies:** Units 14 (recipes, signatures, resolvers all exist)
**Files:**
- Modify: `plugins/compound-engineering/skills/ce-polish-beta/SKILL.md` (Phase 3.2 routing table, Phase 3.3 stub-writer logic, references list at bottom)
**Approach:**
- Phase 3.2 routing table gains four new rows (nuxt, astro, remix, sveltekit)
- Phase 3.2 adds a paragraph under the table: "When the detector returns `<type>@<cwd>`, route by `<type>` as usual, and carry `<cwd>` into the stub-writer for Phase 3.3. When the detector returns `multiple:<type1>@<cwd1>,<type2>@<cwd2>,...`, the interactive prompt lists the `<type>@<cwd>` pairs and asks the user to pick one; headless mode emits the standard `multiple` failure with the pair list appended."
- Phase 3.3 stub-writer logic updated: "For Next/Vite/Nuxt/Astro/Remix/SvelteKit stubs, call `resolve-package-manager.sh` (passing `<cwd>` as the positional arg when present) and substitute the emitted binary and args into `runtimeExecutable` / `runtimeArgs`. When the detector emitted `<type>@<cwd>`, populate the stub's `cwd` field with that value. For port, call `resolve-port.sh [<cwd>] --type <type>` and substitute the emitted port."
- References list at the bottom of SKILL.md gains the three new reference files (Unit 1) and two new scripts (Units 3 and 4)
- `dev-server-detection.md` reference in the "Cascade" section is kept but its description changes to "Port-resolution documentation — the runtime path is `scripts/resolve-port.sh`"
**Patterns to follow:**
- Existing Phase 3.2 table structure and prose (keep the table format, add rows)
- Existing Phase 3.3 stub-writer prose (keep imperative style, add substitution bullets)
- Existing reference list at SKILL.md bottom (alphabetical within scripts/references groups)
**Test scenarios:**
- Test expectation: none — SKILL.md content is model-consumed. The behavior it documents is asserted by Units 2, 3, and 4 unit tests.
**Verification:**
- `bun test tests/skills/ce-polish-beta-*` passes (all old + new tests green)
- `bun run release:validate` passes (SKILL.md structure intact, no broken references)
- Reading SKILL.md Phase 3 start-to-finish, a reader can trace: "detector says `next@apps/web`" → "Phase 3.3 substitutes pm+port+cwd from resolvers into Next stub" → "final stub has `cwd: apps/web`, `runtimeExecutable: pnpm`, `port: 3001`"
- Four new reference files and two new scripts appear in the SKILL.md references list
## High-Level Technical Design
> *This illustrates the intended approach and is directional guidance for review, not implementation specification. The implementing agent should treat it as context, not code to reproduce.*
**Data flow through Phase 3 after the fix:**
```
.claude/launch.json exists? ──yes──▶ use it verbatim ──▶ Phase 3.5
no
detect-project-type.sh
├─ rails | next | vite | procfile | nuxt | astro | remix | sveltekit
│ │
│ ▼
│ load references/dev-server-<type>.md
│ (recipe: command, default port, gotchas)
├─ <type>@<cwd> (monorepo hit, depth ≤ 3)
│ │
│ ▼
│ load recipe + remember cwd for stub-writer
├─ multiple[:<type>@<cwd>,...] (disambiguation needed)
│ │
│ ▼
│ interactive: user picks <type>@<cwd> pair
│ headless: fail with pair list
└─ unknown (no signature anywhere in scan scope)
interactive: ask for exec/args/port
headless: fail
── stub-writer (Phase 3.3) ──────────────────────────
pm = resolve-package-manager.sh [<cwd>] (Next/Vite/Nuxt/Astro/Remix/SvelteKit)
port = resolve-port.sh [<cwd>] --type <type>
stub = template(type).with(
runtimeExecutable = pm.bin,
runtimeArgs = pm.args,
port = port,
cwd = cwd if present
)
```
**Probe-order for `resolve-port.sh` (first hit wins):**
| Rank | Source | Why this order |
|------|--------|----------------|
| 1 | Explicit CLI `--port` | User intent is authoritative |
| 2 | Framework config (`next.config.*` / `vite.config.*` / `nuxt.config.*` / `astro.config.*`) | The framework itself reads this |
| 3 | `config/puma.rb` (rails only) | Rails server actually binds here |
| 4 | `Procfile.dev` web line | What `bin/dev` / foreman actually runs |
| 5 | `docker-compose.yml` web service ports | Container port binding, often authoritative in Docker-first dev |
| 6 | `package.json` `dev`/`start` scripts | Falls back to npm-style CLI flags |
| 7 | `.env*` (quote- and comment-stripped) | Env override, commonly used |
| 8 | Framework default | Last resort, documented table |
## System-Wide Impact
- **Interaction graph:** Phase 3.2 routing consumes detector output; Phase 3.3 stub-writer consumes resolver output. No other phases touch these scripts. Headless mode's "never mutate state" invariant is preserved because all new scripts are read-only probes.
- **Error propagation:** New scripts follow the sentinel-on-stdout + exit-code convention. Phase 3 already handles sentinel outputs from `read-launch-json.sh`; new sentinels (`__NO_PACKAGE_JSON__`) integrate into the same handler shape. Unknown probes fall through to framework defaults (same as today) rather than erroring.
- **State lifecycle risks:** None. No persisted state changes; the stub-writer writes `.claude/launch.json` only in interactive mode with user consent (Phase 3.3 existing behavior, preserved).
- **API surface parity:** Not applicable — this is a skill-internal detection subsystem. The skill's public contract (argument tokens, `checklist.md` format, headless envelope shape) is unchanged.
- **Integration coverage:** Unit 5's verification explicitly traces a full monorepo + pnpm + custom-port scenario end-to-end to catch integration bugs the per-unit tests miss.
- **Unchanged invariants:**
- `.claude/launch.json` always wins over auto-detect (Phase 3.1 unchanged)
- `rails` still beats `procfile` at root (existing precedence preserved)
- Headless mode still never writes `.claude/launch.json`
- The cross-skill `dev-server-detection.md` duplication note (vs `test-browser`) remains manual-sync; this plan does not modify `test-browser`
## Risks & Dependencies
| Risk | Mitigation |
|------|------------|
| Monorepo probe false-positive (e.g., config in a fixture directory) | Exclusion list (`node_modules`, `fixtures`, etc.) in the probe; depth cap at 3; `multiple` output still triggers user disambiguation |
| Framework config regex misses a valid port (e.g., computed expression) | Falls through to `.env` then framework default — same as today, just with more chances to catch a literal. Documented as best-effort |
| Package-manager resolver picks wrong PM (e.g., stale `yarn.lock` in a pnpm-migrated repo) | Priority order follows common-case lockfile precedence; user can override via `launch.json`. Documented in the resolver's header comment |
| New test files slow the suite | Each new test file adds ~10-20 cases using the existing tmp-repo harness (already fast in `ce-polish-beta-dev-server.test.ts`); measurable impact expected < 2 seconds |
| Changing `dev-server-detection.md` breaks a downstream reader | The file is only referenced from within the skill; no external consumers. Grep confirms no cross-skill references before the change lands |
| Dropping `AGENTS.md`/`CLAUDE.md` port grep regresses users relying on it | Very low — the grep was added speculatively and the lossy pattern (`localhost:3000` match) makes it more likely to have surfaced wrong values than correct ones in the wild. Explicit `--port` and `.claude/launch.json` both remain as override paths |
| Polish's `resolve-port.sh` diverges from `test-browser`'s inline cascade and the two drift silently | Unit 4 adds an explicit sync-note block inside `dev-server-detection.md` enumerating the three intentional divergences (quote stripping, comment stripping, no `AGENTS.md`/`CLAUDE.md` grep). A future maintainer who "fixes" `test-browser` by copying polish's cascade, or vice versa, will hit the sync-note first. No automated cross-skill check — acceptable because both skills are internal and the cascade is small |
## Documentation / Operational Notes
- Update PR description on #568 (or a follow-up PR) to note that these gaps are fixed and reference this plan
- No marketplace release entry, version bump, or CHANGELOG edit — release-please handles it
- No user-facing docs outside the skill's own reference tree
- Keep `dev-server-detection.md` as a navigable doc explaining probe order + framework defaults, even though the implementation now lives in `resolve-port.sh`. Reviewers will still land there first when debugging port issues
## Sources & References
- **Origin:** PR feedback from @tmchow on EveryInc/compound-engineering-plugin#568 ([comment](https://github.com/EveryInc/compound-engineering-plugin/pull/568#issuecomment-4254733274))
- **Previous plan:** `docs/plans/2026-04-15-001-feat-ce-polish-skill-plan.md` (feature this fixes)
- **Related files:**
- `plugins/compound-engineering/skills/ce-polish-beta/scripts/detect-project-type.sh`
- `plugins/compound-engineering/skills/ce-polish-beta/references/dev-server-detection.md`
- `plugins/compound-engineering/skills/ce-polish-beta/references/dev-server-next.md`
- `plugins/compound-engineering/skills/ce-polish-beta/references/dev-server-vite.md`
- `plugins/compound-engineering/skills/ce-polish-beta/references/launch-json-schema.md`
- `plugins/compound-engineering/skills/ce-polish-beta/SKILL.md` (Phase 3)
- **Test harness pattern:** `tests/skills/ce-polish-beta-dev-server.test.ts`