v1.91.1.0 fix: harden Impeccable plugin discovery (#2978)

* fix(design-detect): find impeccable installed as a Claude Code plugin

The design-detector probe only ever checked <root>/<SKILL_ROOTS>/skills/impeccable/,
never the Claude Code plugin-cache layout
(<root>/.claude/plugins/cache/<marketplace>/<plugin>/<version>/skills/impeccable/).
A plugin-installed impeccable was therefore invisible: IMPECCABLE_SKILL stayed
absent, the launcher was never found (so the NOT_CACHED run hint never fired),
and its sibling engine was never considered.

Add a plugin-cache walk alongside the existing SKILL_ROOTS walk, sharing the
same presence/launcher/repo-local-exclusion/sibling-engine logic via an
extracted checkSkillDir() helper so both paths stay behaviorally identical.

Fixes #2838

* refactor(design-detect): consolidate newestSemverDir onto safeReaddir

Both did the identical try/catch-around-readdirSync; newestSemverDir now
reuses the new safeReaddir helper instead of duplicating it.

* fix: harden Impeccable plugin discovery and regression fixtures

* test: supply eval mode to the integrated detector callback adapter

---------

Co-authored-by: Som Samantray <som.samantray@gmail.com>
This commit is contained in:
Garry Tan
2026-09-25 14:32:12 -04:00
committed by GitHub
co-authored by Som Samantray
parent 7b534d3e90
commit 2a113ae7e6
18 changed files with 938 additions and 40 deletions
+13
View File
@@ -1,5 +1,18 @@
# Changelog
## [1.91.1.0] - 2026-09-25
### Fixed
- Find Impeccable installed through the Claude Code plugin marketplace, including a trusted custom `CLAUDE_CONFIG_DIR`. Preserve traditional skill installs and the existing explicit-engine, PATH and standalone-cache priority.
- Select plugin versions deterministically with strict semver ordering and support for hash-named versions. Keep a selected installation's launcher, engine and engine version together instead of borrowing an older plugin's engine.
- Use the same strict ordering for the standalone engine cache, retaining its semver-only policy and precedence. Do not follow cache directory symlinks or repository configuration links into unrelated filesystem trees.
- Preserve repository and symlink execution boundaries, sanitize discovery diagnostics, and quote or suppress launcher hints when a filename cannot be represented safely. Discovery never downloads or runs a launcher; engine compatibility warnings and install consent remain unchanged.
- Compare canonical HOME paths at the trust boundary, so home-directory aliases and dotfiles repositories do not hide user-installed engines or admit private home files as scan targets.
- Add plugin discovery, handoff, malformed-version and adversarial-path regressions, plus Windows-safe discovery cases selected by the native Windows test lane.
Includes the plugin-cache discovery contribution from @SomSamantray in #2976.
## [1.90.2.0] - 2026-09-24
**Spend less time waiting for tests.**
+1 -1
View File
@@ -1 +1 @@
1.90.2.0
1.91.1.0
+1 -1
View File
@@ -1,4 +1,4 @@
# gstack digest v1.90.2.0 — regenerate/re-copy after upgrading gstack
# gstack digest v1.91.1.0 — regenerate/re-copy after upgrading gstack
Behavioral rules from gstack (https://github.com/garrytan/gstack), compressed
for agent hosts without a full skill install. The full skills add workflows,
+159 -34
View File
@@ -31,6 +31,8 @@
* ~/{.claude,.agents,.cursor,.gemini,.github,.opencode}/skills/impeccable/scripts/
* ├─ bin/<os>-<arch>/impeccable[.exe] (engine installed beside the launcher) ──► READY
* └─ impeccable (launcher only) ──► IMPECCABLE_NOT_CACHED: <launcher>
* ${CLAUDE_CONFIG_DIR:-~/.claude}/plugins/cache/<marketplace>/<plugin>/<version>/skills/impeccable/
* └─ newest semver skill per plugin, then opaque names in stable order; same trust checks
* <repo|cwd>/<same dirs>/impeccable ──► launcher-present only (IMPECCABLE_NOT_CACHED, no run hint)
* │
* nothing ──► IMPECCABLE_NOT_AVAILABLE
@@ -71,7 +73,7 @@
* helpers they could outlive the kill (known limit).
*
* Env trust: Bun auto-loads a cwd `.env`, so every rendered invocation passes
* `--no-env-file`, and independently IMPECCABLE_BIN / IMPECCABLE_HOME values
* `--no-env-file`, and independently IMPECCABLE_BIN / IMPECCABLE_HOME / CLAUDE_CONFIG_DIR values
* whose realpath lies inside the repo or cwd are ignored (IMPECCABLE_ENV_IGNORED).
*
* Observability: one content-free JSON line per probe/scan appended to
@@ -99,6 +101,7 @@ import { isFrontendPath } from '../lib/frontend-scope';
const WIN = process.platform === 'win32';
const HOME = os.homedir();
const REAL_HOME = realpathOrNull(HOME) ?? HOME;
const ENV = process.env;
/** Where config.yaml lives: the same precedence bin/gstack-config uses. */
@@ -226,7 +229,7 @@ function isEngineName(realFile: string): boolean {
* the user's own installs into "repository-controlled" files.
*/
function isProjectDir(dir: string): boolean {
return !isInside(HOME, dir);
return !isInside(REAL_HOME, dir);
}
/** Under the project the agent is reviewing: the repository, or cwd, when each is a project directory. */
@@ -239,12 +242,27 @@ function semverKey(v: string): number[] | null {
return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
}
function safeReaddir(dir: string): string[] {
try { return fs.readdirSync(dir); } catch { return []; }
}
function strictSemver(name: string): string | null {
const normalized = name.replace(/^v/, '');
const match = normalized.match(/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z.-]+))?(?:\+([0-9A-Za-z.-]+))?$/);
const valid = match && [match[4], match[5]].every(part => part === undefined || part.split('.').every(id => id.length > 0))
&& (!match[4] || match[4].split('.').every(id => !/^0\d+$/.test(id)));
return valid ? normalized : null;
}
function versionOrder(a: { name: string; version: string | null }, b: { name: string; version: string | null }): number {
const order = a.version && b.version ? Bun.semver.order(b.version, a.version) : Number(!!b.version) - Number(!!a.version);
return order || (a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
}
function newestSemverDir(dir: string): string | null {
let entries: string[];
try { entries = fs.readdirSync(dir); } catch { return null; }
const versions = entries.map(e => ({ e, k: semverKey(e) })).filter(x => x.k) as { e: string; k: number[] }[];
versions.sort((a, b) => (b.k[0] - a.k[0]) || (b.k[1] - a.k[1]) || (b.k[2] - a.k[2]));
return versions[0]?.e ?? null;
const versions = safeReaddir(dir).map(name => ({ name, version: strictSemver(name) })).filter(x => x.version);
versions.sort(versionOrder);
return versions[0]?.name ?? null;
}
function readJsonFile(file: string): { ok: true; value: unknown } | { ok: false; missing: boolean } {
@@ -285,6 +303,61 @@ function engineSiblings(launcherDir: string): string[] {
return [...tags].map(t => path.join(launcherDir, 'bin', t, name));
}
/**
* A Claude Code plugin install of impeccable lands at
* <config>/plugins/cache/<marketplace>/<plugin>/<version>/skills/impeccable/,
* never at the traditional <root>/<SKILL_ROOTS entry>/skills/impeccable/ the
* ordinary walk below expects. Marketplace/plugin/version names are not
* predictable, so walk the three levels (github.com/garrytan/gstack/issues/2838).
*/
function pluginCacheImpeccableSkillDirs(configDir: string, step: (s: string) => void): string[] {
const directories = (dir: string): string[] => {
try {
return fs.readdirSync(dir, { withFileTypes: true }).filter(entry => {
if (entry.isDirectory()) return true;
if (entry.isSymbolicLink()) step(`plugin skip ${path.join(dir, entry.name)}: cache traversal does not follow directory symlinks`);
return false;
}).map(entry => entry.name).sort();
} catch (e) {
const code = (e as NodeJS.ErrnoException).code;
if (code !== 'ENOENT') step(`plugin skip ${dir}: ${code}`);
return [];
}
};
const cacheDir = path.join(configDir, 'plugins', 'cache');
const dirs: string[] = [];
step(`plugin cache=${cacheDir}`);
const realConfig = realpathOrNull(configDir);
const realCache = realpathOrNull(cacheDir);
if (realConfig && realCache && !isInside(realCache, realConfig)) {
step(`plugin skip ${cacheDir}: cache resolves outside its configuration directory`);
return dirs;
}
for (const marketplace of directories(cacheDir)) {
const marketplaceDir = path.join(cacheDir, marketplace);
for (const plugin of directories(marketplaceDir)) {
const pluginDir = path.join(marketplaceDir, plugin);
const versions = directories(pluginDir).map(name => ({ name, version: strictSemver(name) }));
versions.sort(versionOrder);
for (const { name, version } of versions) {
const skillDir = path.join(pluginDir, name, 'skills', 'impeccable');
try {
if (!fs.statSync(path.join(skillDir, 'SKILL.md')).isFile()) {
step(`plugin skip ${skillDir}: SKILL.md is not a regular file`);
continue;
}
step(`plugin selected=${skillDir}${version ? '' : ' (opaque version; recency unknown)'}`);
dirs.push(skillDir);
break;
} catch (e) {
step(`plugin skip ${skillDir}: ${(e as NodeJS.ErrnoException).code}`);
}
}
}
}
return dirs;
}
function probe(host: string, verbose = false): Probe {
const cwd = realpathOrNull(process.cwd()) ?? process.cwd();
const repoRoot = gitTopLevel(cwd) ?? cwd;
@@ -302,33 +375,77 @@ function probe(host: string, verbose = false): Probe {
// A launcher inside the repo or cwd counts as "skill present" only: its sibling
// engine is repository-controlled and is never a READY candidate, and the hint
// never tells anyone to run it.
const roots = [...new Set([repoRoot, cwd, HOME])];
let siblingEngine: string | null = null;
let siblingVersion: string | null = null;
const home = HOME;
const roots = [...new Map([repoRoot, cwd, home].map(root => [realpathOrNull(root) ?? root, root])).values()];
let claudeConfig = trustedEnvPath('CLAUDE_CONFIG_DIR', repoRoot, cwd, p.notes, step);
if (claudeConfig) {
try {
if (!fs.statSync(claudeConfig).isDirectory()) {
p.notes.push(`${SENTINEL.ENV_IGNORED}: CLAUDE_CONFIG_DIR is not a directory`);
claudeConfig = null;
}
} catch (e) {
step(`CLAUDE_CONFIG_DIR unavailable: ${(e as NodeJS.ErrnoException).code}`);
claudeConfig = null;
}
}
const installs: { launcher: string; engine?: string; version?: string }[] = [];
const seenSkills = new Set<string>();
let repoLocalLauncher = false;
// Shared by the SKILL_ROOTS walk and the plugin-cache walk below: same
// presence/launcher/repo-local-exclusion/sibling-engine logic either way,
// whichever path convention placed the skill at `skillDir`.
const checkSkillDir = (skillDir: string, rootIsRepo: boolean) => {
const realSkill = realpathOrNull(skillDir);
rootIsRepo ||= !!realSkill && underProject(realSkill, repoRoot, cwd);
const key = `${rootIsRepo ? 'repo:' : ''}${realSkill}`;
if (!realSkill || seenSkills.has(key)) return;
seenSkills.add(key);
if (fs.existsSync(path.join(skillDir, 'SKILL.md'))) p.skillPresent = true;
const launcher = path.join(skillDir, 'scripts', 'impeccable');
if (!fs.existsSync(launcher)) return;
try {
if (!fs.statSync(launcher).isFile()) { step(`launcher skip ${launcher}: not a regular file`); return; }
} catch (e) { step(`launcher skip ${launcher}: ${(e as NodeJS.ErrnoException).code}`); return; }
if (rootIsRepo) { repoLocalLauncher = true; step(`skill skip ${skillDir}: repository-local install`); return; }
const realLauncher = realpathOrNull(launcher);
if (!realLauncher || underProject(realLauncher, repoRoot, cwd)) { repoLocalLauncher = true; step(`launcher skip ${launcher}: repository-local or missing target`); return; }
const install: typeof installs[number] = { launcher };
installs.push(install);
for (const cand of engineSiblings(path.dirname(launcher))) {
const real = realpathOrNull(cand);
if (!real) continue;
if (!isExecutableFile(real) || !isEngineName(real) || underProject(real, repoRoot, cwd)) { step(`engine skip ${cand}: not a trusted executable`); continue; }
install.engine = real;
try {
const v = fs.readFileSync(path.join(path.dirname(launcher), 'VERSION'), 'utf-8').trim();
install.version = semverKey(v) ? v.replace(/^v/, '') : undefined;
} catch { /* no VERSION file */ }
break;
}
};
for (const root of roots) {
const rootIsRepo = underProject(root, repoRoot, cwd);
for (const sub of SKILL_ROOTS) {
const skillDir = path.join(root, sub, 'skills', 'impeccable');
if (fs.existsSync(path.join(skillDir, 'SKILL.md'))) p.skillPresent = true;
const launcher = path.join(skillDir, 'scripts', 'impeccable');
if (!fs.existsSync(launcher)) continue;
if (rootIsRepo) { repoLocalLauncher = true; continue; }
const realLauncher = realpathOrNull(launcher);
if (!realLauncher || underProject(realLauncher, repoRoot, cwd)) { repoLocalLauncher = true; continue; }
p.launcher ??= launcher;
for (const cand of engineSiblings(path.dirname(launcher))) {
const real = realpathOrNull(cand);
if (siblingEngine || !real || !isExecutableFile(real) || !isEngineName(real) || underProject(real, repoRoot, cwd)) continue;
siblingEngine = real;
try {
const v = fs.readFileSync(path.join(path.dirname(launcher), 'VERSION'), 'utf-8').trim();
siblingVersion = semverKey(v) ? v.replace(/^v/, '') : null; // a non-semver VERSION is not trusted as text
} catch { /* no VERSION file */ }
}
if (sub === '.claude' && claudeConfig && root === home) continue;
checkSkillDir(path.join(root, sub, 'skills', 'impeccable'), rootIsRepo);
}
}
step(`skill=${p.skillPresent} launcher=${p.launcher ?? 'none'} repoLocalLauncher=${repoLocalLauncher} sibling=${siblingEngine ?? 'none'}`);
if (claudeConfig) checkSkillDir(path.join(claudeConfig, 'skills', 'impeccable'), false);
const configs = roots.map(root => ({ dir: root === home && claudeConfig ? claudeConfig : path.join(root, '.claude'), repoLocal: underProject(root, repoRoot, cwd) }));
const seenConfigs = new Set<string>();
for (const { dir, repoLocal } of configs) {
const real = realpathOrNull(dir) ?? dir;
if (repoLocal && !underProject(real, repoRoot, cwd)) { step(`plugin skip ${dir}: repository configuration resolves outside the project`); continue; }
if (seenConfigs.has(real)) continue;
seenConfigs.add(real);
for (const skillDir of pluginCacheImpeccableSkillDirs(dir, step)) {
checkSkillDir(skillDir, repoLocal || underProject(real, repoRoot, cwd));
}
}
const bundled = installs.find(install => install.engine);
p.launcher = (bundled ?? installs[0])?.launcher;
step(`skill=${p.skillPresent} launcher=${p.launcher ?? 'none'} repoLocalLauncher=${repoLocalLauncher} sibling=${bundled?.engine ?? 'none'}`);
// Hook manifests, host-aware.
const mine = HOSTS_WITH_HOOKS[host] ?? [];
@@ -423,10 +540,10 @@ function probe(host: string, verbose = false): Probe {
}
// engine beside a HOME-rooted launcher
if (!p.engine && siblingEngine) {
p.engine = siblingEngine;
p.engineVersion = siblingVersion ?? undefined;
p.sentinel = `${SENTINEL.READY}: ${siblingEngine}`;
if (!p.engine && bundled?.engine) {
p.engine = bundled.engine;
p.engineVersion = bundled.version;
p.sentinel = `${SENTINEL.READY}: ${bundled.engine}`;
}
if (p.engine) {
@@ -453,8 +570,13 @@ function probe(host: string, verbose = false): Probe {
const launcher = p.launcher ?? launcherOnPath ?? (repoLocalLauncher ? 'repository-local install' : null);
if (launcher) {
p.sentinel = `${SENTINEL.NOT_CACHED}: ${launcher}`;
const hintPath = p.launcher && !/[\x00-\x1f\x7f`]/.test(p.launcher) && stripControl(p.launcher) === p.launcher && !WIN
? /^[a-zA-Z0-9_./-]+$/.test(p.launcher) ? p.launcher : `'${p.launcher.replaceAll("'", "'\\''")}'`
: null;
const how = p.launcher
? `run \`${p.launcher} detect --help\` once; it fetches the engine version pinned by your install`
? hintPath
? `run \`${hintPath} detect --help\` once in a POSIX shell; it fetches the engine version pinned by your install`
: 'use your Impeccable installation to fetch its pinned engine; no shell command is suggested for this path or platform'
: repoLocalLauncher && !launcherOnPath
? 'the skill is installed inside this repository, and gstack never runs a repository-local launcher; install it under your home directory (`npx impeccable install --scope global` outside the repo) if you want the engine here'
: 'run `npx impeccable install --scope global` yourself (the engine lands beside the skill under your home directory; `npx impeccable detect --help` alone caches it only for npx)';
@@ -630,7 +752,10 @@ function probeLines(p: Probe): string[] {
lines.push(`${SENTINEL.IGNORED_VALUES}: ${p.ignoredValues.join(',')}`);
lines.push(...p.notes);
if (p.steps.length) lines.push(...p.steps.map(s => `${SENTINEL.PROBE_STEP}: ${s}`));
return lines;
return lines.map(line => {
const colon = line.indexOf(':');
return colon < 0 ? line : line.slice(0, colon + 1) + stripControl(line.slice(colon + 1));
});
}
// ── Sanitization ─────────────────────────────────────────────────────────────
+51
View File
@@ -13,6 +13,57 @@
- **Generation-time guard.** The design binary's image prompt carries a "Never:" line built from ten catalog ids.
- **Attribution.** `NOTICE.md` + `licenses/Apache-2.0.txt`.
## Plugin discovery follow-up (2026-09-25)
The detector recognizes Claude Code marketplace installs at
`${CLAUDE_CONFIG_DIR:-~/.claude}/plugins/cache/<marketplace>/<plugin>/<version>/skills/impeccable/`
as well as traditional skill installs. A trusted absolute `CLAUDE_CONFIG_DIR`
replaces the default Claude user profile for both layouts. Relative, missing,
non-directory, or repository-resolving overrides are ignored; `probe --verbose`
explains the fallback. No Claude settings, plugin registry, or hook configuration
is changed.
Within each marketplace/plugin, discovery selects the newest semver directory
with a regular `SKILL.md`, including prerelease ordering. Equal versions use the
directory name as a stable tie-breaker. Hash and `unknown` directories remain
supported after semver candidates in stable name order; their names do not prove
recency. An incomplete directory is skipped, but a valid newer skill without an
engine does not borrow an older plugin's engine. Launcher, bundled engine and
engine `VERSION` stay associated with the selected installation. Plugin version
numbers never replace engine compatibility evidence.
Traditional user skill installs precede plugin candidates; explicit
`IMPECCABLE_BIN`, an accepted PATH binary and the standalone engine cache retain
their existing higher priority. The standalone engine cache shares strict
semver ordering so an rc directory cannot win over its stable release; unlike
plugin discovery, it still rejects opaque names. Multiple marketplaces/plugins are ordered by
name. A ready engine suppresses the install offer; a missing engine retains the
existing consent and never-ask controls. `IMPECCABLE_SKILL: present` still means
files were found on disk, not that a cached plugin is enabled in the current
Claude session. `IMPECCABLE_ENGINE_UNTESTED` remains an honest warning for engines
outside the captured test set.
Discovery is a fixed-depth, read-only filesystem walk. It does not execute
launchers, install plugins, prune caches, or recursively search arbitrary home
directories. Resolved paths inside the reviewed project are not executable
candidates, including symlinked installs. Cache traversal skips directory
symlinks and rejects a cache that resolves outside its configuration directory;
repository configuration links cannot redirect the walk outside the project.
Canonical HOME comparisons preserve
user installs when the home directory is
accessed through an alias, without allowing arbitrary home files as scan targets.
Verbose output records rejected paths and filesystem errors without breaking the
sentinel protocol. Runnable launcher
hints use POSIX-shell quoting; control-character or Markdown-breaking names and
Windows receive non-command guidance instead. The raw path used for engine
execution is separate from its sanitized display.
Regressions live in `test/gstack-design-detect.test.ts`; the plugin handoff agent
case in `test/skill-e2e-design.test.ts` exercises the actual discovery-to-report
path without an explicit engine override. Native Windows executable behavior and
macOS path alias behavior require their own platform results; Linux fixture
passes are not evidence of those native runs.
## CEO plan (promoted)
### CEO Plan: impeccable.style interop for gstack's design skills
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "gstack",
"version": "1.90.2",
"version": "1.91.1",
"description": "Garry's Stack — Claude Code skills + fast headless browser. One repo, one install, entire AI engineering workflow.",
"license": "MIT",
"type": "module",
+12
View File
@@ -37,6 +37,15 @@ import {
LARGE_REPO_FILE_THRESHOLD,
} from "../lib/code-intelligence";
function isolateGitRemote(repo: string, url: string): void {
const git = (...args: string[]) => execFileSync("git", args, {
cwd: repo, encoding: "utf8", timeout: 10_000,
}).trim();
expect(git("config", "--get", "remote.origin.url")).toBe(url);
git("config", "--local", `url.${url}.insteadOf`, url);
expect(git("remote", "get-url", "origin")).toBe(url);
}
describe("capability matrix", () => {
test("every provider advertises the four required capabilities", () => {
for (const p of [new GbrainProvider(), new SourcebotProvider(), new GraphifyProvider()]) {
@@ -402,6 +411,7 @@ describe("consent unification — deny tier wins (R1)", () => {
const git = (...a: string[]) => execFileSync("git", a, { cwd: repo, timeout: 30_000 });
git("init", "-q", ".");
git("remote", "add", "origin", url);
isolateGitRemote(repo, url);
return repo;
}
const POLICY_BIN = path.join(import.meta.dir, "..", "bin", "gstack-gbrain-repo-policy");
@@ -500,6 +510,7 @@ describe("read-only repo policy blocks write-class CLI index (R2)", () => {
fs.mkdirSync(repo, { recursive: true });
execFileSync("git", ["init", "-q", "."], { cwd: repo });
execFileSync("git", ["remote", "add", "origin", URL], { cwd: repo });
isolateGitRemote(repo, URL);
setProvider("gbrain", env);
setConsent(repo, true, env);
execFileSync(POLICY_BIN, ["set", URL, "read-only"], { env, encoding: "utf-8" });
@@ -702,6 +713,7 @@ describe("CLI search consent gate (gbrain provider, honest refusal message)", ()
fs.mkdirSync(repo, { recursive: true });
execFileSync("git", ["init", "-q", "."], { cwd: repo });
execFileSync("git", ["remote", "add", "origin", URL], { cwd: repo });
isolateGitRemote(repo, URL);
env = { ...process.env, GSTACK_HOME: home, PATH: `${shimDir}:${process.env.PATH}` };
setProvider("gbrain", env);
setRoot("gbrain", repo, env);
+1 -1
View File
@@ -58,7 +58,7 @@ async function exercise(modes: Mode[], retention?: 'directory' | 'run-id' | 'bot
},
};
const args = {
ROOT, fs: localFs, os: { tmpdir: () => scratch }, path, expect, CAPTURE_MS, CAPTURE_LONG_MS,
ROOT, fs: localFs, os: { tmpdir: () => scratch }, path, expect, CAPTURE_MS, CAPTURE_LONG_MS, evalsEnabled: true,
process: { ...process, env: { ...process.env, ...env } }, resolveEvalModel,
getProjectEvalDir: () => artifactRoot,
console: { ...console, log: (...args: any[]) => notices.push(args.join(' ')), error: (...args: any[]) => notices.push(args.join(' ')) },
+1 -1
View File
@@ -29,7 +29,7 @@ describe('AO completed manual DX handoff preserves report freshness',()=>{
expect(E2E_TOUCHFILES[owner]).toContain('test/fixtures/dx-manual-handoff-ao.json');
}
const arrays=[...Object.values(E2E_TOUCHFILES),...Object.values(LLM_JUDGE_TOUCHFILES),GLOBAL_TOUCHFILES];
expect(arrays).toHaveLength(234);
expect(arrays).toHaveLength(235);
for(const values of arrays)for(let i=0;i<values.length;i++)expect(typeof values[i]).toBe('string');
});
test('exact owned report precedes navigation only, with the current Exit gate recognized',()=>{
+2
View File
@@ -8,6 +8,7 @@ describe('fake impeccable engine selection', () => {
'design-html-slop-gate',
'design-review-detector-shim',
'design-review-detector-shim-dom',
'design-review-plugin-handoff',
'review-design-lite',
];
@@ -28,6 +29,7 @@ describe('fake impeccable engine selection', () => {
['design-html-slop-gate', 'periodic'],
['design-review-detector-shim', 'gate'],
['design-review-detector-shim-dom', 'gate'],
['design-review-plugin-handoff', 'gate'],
['review-design-lite', 'periodic'],
]);
});
+31
View File
@@ -25,6 +25,17 @@ const BIN = path.join(ROOT, 'bin', 'gstack-gbrain-repo-policy');
let tmpHome: string;
function isolateGitRemote(repo: string, url: string): void {
const git = (...args: string[]) => {
const result = spawnSync('git', args, { cwd: repo, encoding: 'utf8', timeout: 10_000 });
expect(result.status).toBe(0);
return result.stdout.trim();
};
expect(git('config', '--get', 'remote.origin.url')).toBe(url);
git('config', '--local', `url.${url}.insteadOf`, url);
expect(git('remote', 'get-url', 'origin')).toBe(url);
}
function run(args: string[], opts: { env?: Record<string, string> } = {}) {
const res = spawnSync(BIN, args, {
env: { ...process.env, GSTACK_HOME: tmpHome, ...(opts.env || {}) },
@@ -54,6 +65,25 @@ afterEach(() => {
fs.rmSync(tmpHome, { recursive: true, force: true });
});
test('fixture origin isolation overrides a controlled URL rewrite without changing the stored remote', () => {
const repo = path.join(tmpHome, 'repo');
fs.mkdirSync(repo);
const git = (...args: string[]) => {
const result = spawnSync('git', args, { cwd: repo, encoding: 'utf8', timeout: 10_000 });
expect(result.status).toBe(0);
return result.stdout.trim();
};
const url = 'https://fixture.invalid/org/repo.git';
git('init', '-q');
git('remote', 'add', 'origin', url);
git('config', '--local', 'url.https://mirror.invalid/.insteadOf', 'https://fixture.invalid/');
expect(git('config', '--get', 'remote.origin.url')).toBe(url);
expect(git('remote', 'get-url', 'origin')).toBe('https://mirror.invalid/org/repo.git');
isolateGitRemote(repo, url);
expect(git('config', '--get', 'remote.origin.url')).toBe(url);
expect(git('remote', 'get-url', 'origin')).toBe(url);
});
describe('normalize', () => {
test('strips https:// and .git', () => {
const r = run(['normalize', 'https://github.com/foo/bar.git']);
@@ -293,6 +323,7 @@ describe('gstack-gbrain-sync code stage honors the repo policy (#2140 sync path)
spawnSync('git', args, { cwd: repoDir, encoding: 'utf-8', timeout: 30_000 });
git('init', '-q', '.');
git('remote', 'add', 'origin', REPO_URL);
isolateGitRemote(repoDir, REPO_URL);
fs.writeFileSync(path.join(repoDir, 'README.md'), 'fixture\n');
git('add', '-A');
git('-c', 'user.email=t@t', '-c', 'user.name=t', 'commit', '-qm', 'fixture');
+61
View File
@@ -0,0 +1,61 @@
import { test, expect } from 'bun:test';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { spawnSync } from 'child_process';
import { fileURLToPath } from 'url';
import { SENTINEL } from '../lib/design-detect-contract';
const detector = fileURLToPath(new URL('../bin/gstack-design-detect.ts', import.meta.url));
test.each([
{ versions: ['4.3.1', '4.10.0'], selected: '4.10.0', custom: false },
{ versions: ['4.3.1', '4.10.0'], selected: '4.10.0', custom: true },
{ versions: ['4.3.1-beta.1', '4.3.1'], selected: '4.3.1', custom: false },
{ versions: ['99.0.0-', '4.3.1'], selected: '4.3.1', custom: false },
{ versions: ['a1b2c3d4'], selected: 'a1b2c3d4', custom: true },
{ versions: ['unknown'], selected: 'unknown', custom: false },
])('native plugin discovery %j', ({ versions, selected, custom }) => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-plugin-native-'));
const home = path.join(root, 'home');
const cwd = path.join(root, 'project');
const config = custom ? path.join(root, 'custom config') : path.join(home, '.claude');
for (const dir of [home, cwd, config]) fs.mkdirSync(dir, { recursive: true });
const env: Record<string, string> = {
HOME: home, USERPROFILE: home, PATH: path.dirname(process.execPath),
GSTACK_HOME: path.join(root, 'gstack'), IMPECCABLE_HOME: path.join(root, 'engine-cache'),
};
for (const key of ['SystemRoot', 'SYSTEMROOT', 'TEMP', 'TMP']) if (process.env[key]) env[key] = process.env[key]!;
if (custom) env.CLAUDE_CONFIG_DIR = config;
try {
const launchers = new Map<string, string>();
for (const version of versions) {
const skill = path.join(config, 'plugins', 'cache', 'market', 'impeccable', version, 'skills', 'impeccable');
fs.mkdirSync(path.join(skill, 'scripts'), { recursive: true });
fs.writeFileSync(path.join(skill, 'SKILL.md'), '# fixture\n');
const launcher = path.join(skill, 'scripts', 'impeccable');
fs.writeFileSync(launcher, 'This fixture is not an executable.\n');
launchers.set(version, launcher);
}
if (custom) {
const stale = path.join(home, '.claude', 'skills', 'impeccable');
fs.mkdirSync(stale, { recursive: true });
fs.writeFileSync(path.join(stale, 'SKILL.md'), '# stale default profile\n');
}
const probe = () => spawnSync(process.execPath, ['--no-env-file', 'run', detector, 'probe'], {
cwd, env, encoding: 'utf-8', timeout: 30_000,
});
const found = probe();
expect(found.status).toBe(0);
const expectedLauncher = custom
? path.join(fs.realpathSync(config), path.relative(config, launchers.get(selected)!))
: launchers.get(selected)!;
expect(found.stdout.split('\n')[0]).toBe(`${SENTINEL.NOT_CACHED}: ${expectedLauncher}`);
expect(found.stdout).toContain(`${SENTINEL.SKILL}: present`);
expect(found.stdout).not.toContain(`${SENTINEL.READY}:`);
fs.rmSync(path.join(config, 'plugins'), { recursive: true });
const removed = probe();
expect(removed.status).toBe(0);
expect(removed.stdout).toContain(`${SENTINEL.SKILL}: absent`);
} finally { fs.rmSync(root, { recursive: true, force: true }); }
});
+414
View File
@@ -342,6 +342,420 @@ describe('probe', () => {
});
});
describe('plugin-cache selection and trust', () => {
function fixture(check: (home: string, config: string) => void) {
const home = fs.mkdtempSync(path.join(SANDBOX, 'plugin-home-'));
try { check(home, path.join(home, '.claude')); }
finally { fs.rmSync(home, { recursive: true, force: true }); }
}
function plugin(config: string, version: string, opts: { engine?: boolean; engineVersion?: string; marketplace?: string; name?: string } = {}) {
const skill = path.join(config, 'plugins', 'cache', opts.marketplace ?? 'impeccable', opts.name ?? 'impeccable', version, 'skills', 'impeccable');
const scripts = path.join(skill, 'scripts');
fs.mkdirSync(scripts, { recursive: true });
fs.writeFileSync(path.join(skill, 'SKILL.md'), '# impeccable\n');
const marker = path.join(config, 'launcher-ran');
const launcher = path.join(scripts, 'impeccable');
fs.writeFileSync(launcher, `#!/bin/sh\nprintf ran > ${JSON.stringify(marker)}\n`);
fs.chmodSync(launcher, 0o755);
fs.writeFileSync(path.join(scripts, 'VERSION'), `${opts.engineVersion ?? '0.1.3'}\n`);
const engine = path.join(scripts, 'bin', `${process.platform}-${process.arch}`, POSIX ? 'impeccable' : 'impeccable.exe');
if (opts.engine) {
fs.mkdirSync(path.dirname(engine), { recursive: true });
fs.copyFileSync(FAKE, engine);
fs.chmodSync(engine, 0o755);
}
return { skill, scripts, launcher, engine, marker };
}
test.each([
[['4.3.1', '4.10.0'], '4.10.0'],
[['4.10.0', '4.3.1'], '4.10.0'],
[['4.3.1-beta.1', '4.3.1'], '4.3.1'],
[['4.4.0-beta.2', '4.4.0-beta.10'], '4.4.0-beta.10'],
[['unknown', 'a1b2c3d4'], 'a1b2c3d4'],
[['a1b2c3d4'], 'a1b2c3d4'],
[['unknown'], 'unknown'],
[['unknown', '4.3.1'], '4.3.1'],
[['v4.3.1', '4.3.1'], '4.3.1'],
[['99.0.0-', '4.3.1'], '4.3.1'],
[['99.0.0-beta..1', '4.3.1'], '4.3.1'],
[['99.0.0-01', '4.3.1'], '4.3.1'],
[['99.0.0+build..1', '4.3.1'], '4.3.1'],
[['99.0.0-'], '99.0.0-'],
[['4.3.1+build.2', '4.3.1+build.1'], '4.3.1+build.1'],
] as [string[], string][])('orders plugin versions %j and selects %s', (versions, expected) => {
fixture((home, config) => {
const installs = new Map(versions.map(v => [v, plugin(config, v)]));
const r = run(['probe', '--verbose'], { env: { HOME: home } });
expect(r.code).toBe(0);
expect(lines(r.out)[0]).toBe(`${SENTINEL.NOT_CACHED}: ${installs.get(expected)!.launcher}`);
expect(r.out).toContain(`${SENTINEL.SKILL}: present`);
if (!/^[v\d]/.test(expected)) expect(r.out).toContain('opaque version; recency unknown');
for (const install of installs.values()) expect(fs.existsSync(install.marker)).toBe(false);
});
});
test.skipIf(!POSIX)('selected plugin engine and version match; scans produce handoff data without running launchers', () => {
fixture((home, config) => {
plugin(config, '4.3.1', { engine: true, engineVersion: '0.1.3' });
const current = plugin(config, '4.10.0', { engine: true, engineVersion: '0.1.5' });
const env = { HOME: home };
const p = run(['probe'], { env });
expect(lines(p.out)[0]).toBe(`${SENTINEL.READY}: ${fs.realpathSync(current.engine)}`);
expect(p.out).toContain(`${SENTINEL.ENGINE_UNTESTED}: 0.1.5`);
expect(p.out).not.toContain(SENTINEL.INSTALL_OFFER);
const log = path.join(home, 'engine-args.jsonl');
const scan = run(['scan', 'src/styles.css'], { env: { ...env, IMPECCABLE_FAKE_LOG: log } });
expect(scan.code).toBe(2);
expect(scan.err).toContain(`${SENTINEL.SKILL}: present`);
expect(scan.err).toContain('handoff=/impeccable');
expect(JSON.parse(scan.out).engine).toBe(fs.realpathSync(current.engine));
expect(JSON.parse(scan.out).engineVersion).toBe('0.1.5');
expect(fs.readFileSync(log, 'utf-8').trim().split('\n')).toHaveLength(1);
expect(fs.existsSync(current.marker)).toBe(false);
});
});
test('a newer launcher-only plugin never borrows an older bundled engine', () => {
fixture((home, config) => {
plugin(config, '4.3.1', { engine: true });
const current = plugin(config, '4.10.0');
const r = run(['probe'], { env: { HOME: home } });
expect(lines(r.out)[0]).toBe(`${SENTINEL.NOT_CACHED}: ${current.launcher}`);
expect(r.out).not.toContain(SENTINEL.READY);
});
});
test('custom Claude configuration replaces the default profile for plugins and legacy skills', () => {
fixture((home, config) => {
const ignored = plugin(config, '99.0.0', { engine: true });
const custom = path.join(home, 'custom claude');
const selected = plugin(custom, '4.3.1', { engine: true });
const env = { HOME: home, CLAUDE_CONFIG_DIR: custom };
const r = run(['probe'], { env });
expect(lines(r.out)[0]).toBe(`${SENTINEL.READY}: ${fs.realpathSync(selected.engine)}`);
expect(r.out).not.toContain(ignored.engine);
fs.rmSync(path.join(custom, 'plugins'), { recursive: true });
expect(run(['probe'], { env }).out).toContain(`${SENTINEL.SKILL}: absent`);
const legacy = path.join(custom, 'skills', 'impeccable');
fs.mkdirSync(path.dirname(legacy), { recursive: true });
fs.cpSync(ignored.skill, legacy, { recursive: true });
const legacyEngine = path.join(legacy, 'scripts', 'bin', `${process.platform}-${process.arch}`, path.basename(ignored.engine));
expect(lines(run(['probe'], { env }).out)[0]).toBe(`${SENTINEL.READY}: ${fs.realpathSync(legacyEngine)}`);
});
});
test.each(['relative', 'missing', 'file', 'project'])('invalid %s custom configuration uses safe default discovery', kind => {
fixture((home, config) => {
const good = plugin(config, '4.3.1');
const file = path.join(home, 'not-directory');
fs.writeFileSync(file, 'x');
const override = { relative: '.claude', missing: path.join(home, 'missing'), file, project: REPO }[kind]!;
const r = run(['probe', '--verbose'], { env: { HOME: home, CLAUDE_CONFIG_DIR: override } });
expect(lines(r.out)[0]).toBe(`${SENTINEL.NOT_CACHED}: ${good.launcher}`);
expect(r.out).toContain('CLAUDE_CONFIG_DIR');
});
});
test('ignores incomplete versions, non-directory entries and directory-shaped skill markers', () => {
fixture((home, config) => {
const good = plugin(config, '4.3.1');
const bad = plugin(config, '99.0.0');
fs.rmSync(path.join(bad.skill, 'SKILL.md'));
fs.mkdirSync(path.join(bad.skill, 'SKILL.md'));
const pluginRoot = path.resolve(good.skill, '../../..');
fs.writeFileSync(path.join(pluginRoot, '100.0.0'), 'not a version directory');
fs.mkdirSync(path.join(pluginRoot, '101.0.0'));
const r = run(['probe', '--verbose'], { env: { HOME: home } });
expect(lines(r.out)[0]).toBe(`${SENTINEL.NOT_CACHED}: ${good.launcher}`);
expect(r.out).toContain('SKILL.md is not a regular file');
expect(r.out).toContain('ENOENT');
fs.rmSync(path.join(config, 'plugins'), { recursive: true });
fs.mkdirSync(path.join(config, 'plugins'));
fs.writeFileSync(path.join(config, 'plugins', 'cache'), 'not a directory');
const malformed = run(['probe', '--verbose'], { env: { HOME: home } });
expect(malformed.code).toBe(0);
expect(malformed.out).toContain('ENOTDIR');
expect(malformed.out).toContain(`${SENTINEL.SKILL}: absent`);
});
});
test.skipIf(!POSIX || process.getuid?.() === 0)('unreadable cache is diagnosed without crashing', () => {
fixture((home, config) => {
plugin(config, '4.3.1');
const cache = path.join(config, 'plugins', 'cache');
fs.chmodSync(cache, 0o000);
try {
const r = run(['probe', '--verbose'], { env: { HOME: home } });
expect(r.code).toBe(0);
expect(r.out).toContain('EACCES');
} finally { fs.chmodSync(cache, 0o755); }
});
});
test.skipIf(!POSIX)('project-resolving launcher, engine, cache and config symlinks are never executed', () => {
for (const kind of ['launcher', 'engine', 'cache', 'config']) {
fixture((home, config) => {
const install = plugin(config, '4.3.1', { engine: true });
const projectDir = fs.mkdtempSync(path.join(REPO, 'plugin-owned-'));
const marker = path.join(home, 'unsafe-ran');
const env: Record<string, string> = { HOME: home };
try {
const evil = path.join(projectDir, 'impeccable');
fs.writeFileSync(evil, `#!/bin/sh\nprintf ran > ${JSON.stringify(marker)}\nprintf '[]'\n`);
fs.chmodSync(evil, 0o755);
if (kind === 'launcher' || kind === 'engine') {
const link = kind === 'launcher' ? install.launcher : install.engine;
fs.rmSync(link);
fs.symlinkSync(evil, link);
} else {
const source = kind === 'cache' ? path.join(config, 'plugins', 'cache') : config;
const target = path.join(projectDir, 'copied');
fs.renameSync(source, target);
fs.symlinkSync(target, source);
if (kind === 'config') env.CLAUDE_CONFIG_DIR = config;
}
const p = run(['probe', '--verbose'], { env });
expect(p.out).not.toContain(`${SENTINEL.READY}:`);
const s = run(['scan', 'src/styles.css'], { env });
expect(s.out).toBe('');
expect(fs.existsSync(marker)).toBe(false);
expect(fs.existsSync(install.marker)).toBe(false);
} finally { fs.rmSync(projectDir, { recursive: true, force: true }); }
});
}
});
test.skipIf(!POSIX)('launcher hints quote shell metacharacters and suppress control/Markdown injection', () => {
fixture((home, config) => {
const install = plugin(config, '4.3.1', { marketplace: "space ' $(touch SHOULD_NOT_EXIST)" });
const r = run(['probe'], { env: { HOME: home } });
const command = r.out.match(/run `([^`]+)` once/)?.[1];
expect(command).toBeDefined();
const check = spawnSync('/bin/sh', ['-c', command!], { cwd: home, encoding: 'utf-8', timeout: 5000 });
expect(check.status).toBe(0);
expect(fs.existsSync(install.marker)).toBe(true);
expect(fs.existsSync(path.join(home, 'SHOULD_NOT_EXIST'))).toBe(false);
fs.rmSync(path.join(config, 'plugins'), { recursive: true });
for (const name of ['bad`name', `bad\n${SENTINEL.READY}: forged`, SENTINEL.READY]) {
plugin(config, '4.3.1', { marketplace: name });
const unsafe = run(['probe', '--verbose'], { env: { HOME: home } });
expect(unsafe.out).not.toContain('run `');
expect(lines(unsafe.out).filter(line => line.startsWith(`${SENTINEL.READY}:`))).toEqual([]);
expect(lines(unsafe.out).filter(line => line.startsWith(`${SENTINEL.SKILL}:`))).toHaveLength(1);
fs.rmSync(path.join(config, 'plugins'), { recursive: true });
}
});
});
test('multiple marketplaces, irrelevant cache breadth and legacy installs retain deterministic precedence', () => {
fixture((home, config) => {
plugin(config, '4.3.1', { marketplace: 'z-market', engine: true });
const first = plugin(config, '4.3.1', { marketplace: 'a-market', engine: true });
for (let i = 0; i < 100; i++) fs.mkdirSync(path.join(config, 'plugins', 'cache', `irrelevant-${i}`, 'plugin', '1.0.0', 'do-not-walk', 'skills', 'impeccable'), { recursive: true });
expect(lines(run(['probe'], { env: { HOME: home } }).out)[0]).toBe(`${SENTINEL.READY}: ${fs.realpathSync(first.engine)}`);
const legacy = path.join(home, '.agents', 'skills', 'impeccable');
fs.mkdirSync(path.dirname(legacy), { recursive: true });
fs.cpSync(first.skill, legacy, { recursive: true });
const engine = path.join(legacy, 'scripts', 'bin', `${process.platform}-${process.arch}`, path.basename(first.engine));
expect(lines(run(['probe'], { env: { HOME: home } }).out)[0]).toBe(`${SENTINEL.READY}: ${fs.realpathSync(engine)}`);
expect(lines(run(['probe'], { env: { HOME: home, IMPECCABLE_BIN: FAKE } }).out)[0]).toBe(`${SENTINEL.READY}: ${fs.realpathSync(FAKE)}`);
});
});
test('plugin discovery preserves standalone cache priority, off and never-ask behavior', () => {
fixture((home, config) => {
const install = plugin(config, '4.3.1', { engine: true });
const cache = path.join(home, 'engine-cache');
const engine = path.join(cache, 'bin', '0.1.3', path.basename(install.engine));
fs.mkdirSync(path.dirname(engine), { recursive: true });
fs.copyFileSync(FAKE, engine);
fs.chmodSync(engine, 0o755);
const state = path.join(home, 'state');
fs.mkdirSync(state);
const env = { HOME: home, GSTACK_HOME: state, IMPECCABLE_HOME: cache };
const p = run(['probe'], { env });
expect(lines(p.out)[0]).toBe(`${SENTINEL.READY}: ${fs.realpathSync(engine)}`);
expect(p.out).not.toContain(SENTINEL.INSTALL_OFFER);
fs.writeFileSync(path.join(state, 'config.yaml'), 'design_detector: off\n');
const off = run(['probe'], { env });
expect(lines(off.out)[0]).toBe(SENTINEL.DISABLED);
expect(off.out).toContain(`${SENTINEL.SKILL}: present`);
fs.writeFileSync(path.join(state, 'config.yaml'), 'design_detector_install_prompted: true\n');
fs.rmSync(cache, { recursive: true });
fs.rmSync(install.engine);
const nc = run(['probe'], { env });
expect(lines(nc.out)[0]).toBe(`${SENTINEL.NOT_CACHED}: ${install.launcher}`);
expect(nc.out).not.toContain(SENTINEL.INSTALL_OFFER);
expect(nc.out).not.toContain(SENTINEL.HINT);
fs.rmSync(path.join(config, 'plugins'), { recursive: true });
expect(lines(run(['probe'], { env }).out)[0]).toBe(SENTINEL.NOT_AVAILABLE);
expect(fs.existsSync(install.marker)).toBe(false);
});
});
test.skipIf(!POSIX).each([false, true])('symlinked HOME preserves user installs without trusting arbitrary home scan targets (dotfiles=%s)', dotfiles => {
fixture((home, config) => {
const install = plugin(config, '4.3.1', { engine: true });
const alias = `${home}-alias`;
fs.symlinkSync(home, alias, 'dir');
try {
if (dotfiles) git(home, 'init', '-q');
const log = path.join(home, 'engine-calls');
const opts = { cwd: home, env: { HOME: alias, IMPECCABLE_FAKE_LOG: log } };
expect(lines(run(['probe'], opts).out)[0]).toBe(`${SENTINEL.READY}: ${fs.realpathSync(install.engine)}`);
const target = path.join(home, 'private.css');
fs.writeFileSync(target, 'body { color: red; }');
const scan = run(['scan', target], opts);
expect(scan.code).toBe(0);
expect(scan.err).toContain(`${SENTINEL.DETECT_REFUSED}: ${target}`);
expect(scan.err).toContain(SENTINEL.DETECT_NO_TARGETS);
expect(scan.out).toBe('');
expect(fs.existsSync(log)).toBe(false);
expect(fs.existsSync(install.marker)).toBe(false);
} finally { fs.rmSync(alias); }
});
});
test.skipIf(!POSIX).each(['config', 'cache', 'marketplace', 'version'])('repository %s symlinks cannot redirect plugin traversal to a user install', kind => {
fixture((home) => {
const externalConfig = path.join(home, 'external-config');
plugin(externalConfig, '4.3.1', { engine: true });
const config = path.join(REPO, '.claude');
const tail = {
config: '', cache: 'plugins/cache', marketplace: 'plugins/cache/impeccable',
version: 'plugins/cache/impeccable/impeccable/4.3.1',
}[kind]!;
const link = path.join(config, tail);
fs.mkdirSync(path.dirname(link), { recursive: true });
fs.symlinkSync(path.join(externalConfig, tail), link, 'dir');
const log = path.join(home, 'engine-ran');
try {
const env = { HOME: home, IMPECCABLE_FAKE_LOG: log };
const p = run(['probe', '--verbose'], { env });
expect(lines(p.out)[0]).toBe(SENTINEL.NOT_AVAILABLE);
expect(p.out).toContain(`${SENTINEL.SKILL}: absent`);
expect(p.out).not.toContain('plugin selected=');
expect(p.out).toContain('plugin skip');
expect(run(['scan', 'src/styles.css'], { env }).out).toBe('');
expect(fs.existsSync(log)).toBe(false);
} finally { fs.rmSync(config, { recursive: true, force: true }); }
});
});
test('standalone cache shares strict version ordering but never treats opaque names as versions', () => {
fixture((home) => {
const cache = path.join(home, 'cache');
let stable = '';
for (const version of ['0.1.3-rc.1', 'v0.1.3', '99.0.0-', 'unknown']) {
const engine = path.join(cache, 'bin', version, POSIX ? 'impeccable' : 'impeccable.exe');
fs.mkdirSync(path.dirname(engine), { recursive: true });
fs.copyFileSync(FAKE, engine);
fs.chmodSync(engine, 0o755);
if (version === 'v0.1.3') stable = engine;
}
const p = run(['probe'], { env: { HOME: home, IMPECCABLE_HOME: cache } });
expect(lines(p.out)[0]).toBe(`${SENTINEL.READY}: ${fs.realpathSync(stable)}`);
expect(p.out).not.toContain(SENTINEL.ENGINE_UNTESTED);
fs.rmSync(path.join(cache, 'bin', 'v0.1.3'), { recursive: true });
fs.rmSync(path.join(cache, 'bin', '0.1.3-rc.1'), { recursive: true });
expect(lines(run(['probe'], { env: { HOME: home, IMPECCABLE_HOME: cache } }).out)[0]).toBe(SENTINEL.NOT_AVAILABLE);
});
});
});
describe('plugin-cache impeccable install', () => {
// A Claude Code plugin install places impeccable at
// <HOME>/.claude/plugins/cache/<marketplace>/<plugin>/<version>/skills/impeccable/,
// never at the traditional <HOME>/.claude/skills/impeccable/ the SKILL_ROOTS
// walk expects (regression for github.com/garrytan/gstack/issues/2838).
function pluginCacheDir(root: string, marketplace = 'impeccable', plugin = 'impeccable', version = '4.3.1') {
return path.join(root, '.claude', 'plugins', 'cache', marketplace, plugin, version, 'skills', 'impeccable');
}
test('SKILL.md present, no launcher → IMPECCABLE_SKILL: present, no crash, no READY', () => {
const home = path.join(SANDBOX, 'fake-home');
const skillDir = pluginCacheDir(home);
fs.mkdirSync(skillDir, { recursive: true });
fs.writeFileSync(path.join(skillDir, 'SKILL.md'), '# impeccable\n');
try {
const r = run(['probe']);
expect(r.out).toContain(`${SENTINEL.SKILL}: present`);
expect(r.out).not.toContain(SENTINEL.READY);
} finally {
fs.rmSync(path.join(home, '.claude'), { recursive: true, force: true });
}
});
test.skipIf(!POSIX)('plugin-cache launcher without engine → NOT_CACHED naming the plugin-cache launcher; with sibling engine → READY + VERSION', () => {
const home = path.join(SANDBOX, 'fake-home');
const skillDir = pluginCacheDir(home);
const scripts = path.join(skillDir, 'scripts');
fs.mkdirSync(scripts, { recursive: true });
fs.writeFileSync(path.join(skillDir, 'SKILL.md'), '# impeccable\n');
fs.writeFileSync(path.join(scripts, 'impeccable'), '#!/bin/sh\necho "would download"\n');
fs.chmodSync(path.join(scripts, 'impeccable'), 0o755);
fs.writeFileSync(path.join(scripts, 'VERSION'), '0.1.3\n');
try {
const r = run(['probe']);
expect(lines(r.out)[0]).toBe(`${SENTINEL.NOT_CACHED}: ${path.join(scripts, 'impeccable')}`);
expect(r.out).toContain(`${SENTINEL.SKILL}: present`);
expect(r.out).toContain(`run \`${path.join(scripts, 'impeccable')} detect --help\` once`);
expect(r.out).not.toContain('npx impeccable');
expect(r.out).not.toContain('would download');
const sib = path.join(scripts, 'bin', `${process.platform}-${process.arch}`);
fs.mkdirSync(sib, { recursive: true });
fs.copyFileSync(FAKE, path.join(sib, 'impeccable'));
fs.chmodSync(path.join(sib, 'impeccable'), 0o755);
const r2 = run(['probe']);
expect(lines(r2.out)[0]).toBe(`${SENTINEL.READY}: ${fs.realpathSync(path.join(sib, 'impeccable'))}`);
expect(r2.out).not.toContain(SENTINEL.ENGINE_UNTESTED);
expect(r2.out).not.toContain(SENTINEL.HINT);
} finally {
fs.rmSync(path.join(home, '.claude'), { recursive: true, force: true });
}
});
test.skipIf(!POSIX)('a plugin-cache install committed INSIDE the repository is never executed: skill-present only, launcher never runs', () => {
const skillDir = pluginCacheDir(REPO, 'acme', 'impeccable', '1.0.0');
const scripts = path.join(skillDir, 'scripts');
const sib = path.join(scripts, 'bin', `${process.platform}-${process.arch}`);
fs.mkdirSync(sib, { recursive: true });
fs.writeFileSync(path.join(skillDir, 'SKILL.md'), '# impeccable\n');
fs.writeFileSync(path.join(scripts, 'impeccable'), '#!/bin/sh\necho "would download"\n');
fs.chmodSync(path.join(scripts, 'impeccable'), 0o755);
fs.writeFileSync(path.join(scripts, 'VERSION'), '0.1.3\n');
const marker = path.join(SANDBOX, 'repo-plugin-engine-ran.txt');
fs.writeFileSync(path.join(sib, 'impeccable'), `#!/bin/sh\necho ran > ${JSON.stringify(marker)}\necho "[]"\n`);
fs.chmodSync(path.join(sib, 'impeccable'), 0o755);
try {
const r = run(['probe']);
expect(lines(r.out)[0]).toBe(`${SENTINEL.NOT_CACHED}: repository-local install`);
expect(r.out).toContain(`${SENTINEL.SKILL}: present`);
expect(r.out).toContain('never runs a repository-local launcher');
expect(fs.existsSync(marker)).toBe(false);
} finally {
fs.rmSync(path.join(REPO, '.claude'), { recursive: true, force: true });
}
});
test('multiple plugin-cache entries (different marketplace/plugin/version) coexist without throwing', () => {
const home = path.join(SANDBOX, 'fake-home');
fs.mkdirSync(pluginCacheDir(home, 'marketA', 'impeccable', '1.0.0'), { recursive: true });
fs.writeFileSync(path.join(pluginCacheDir(home, 'marketA', 'impeccable', '1.0.0'), 'SKILL.md'), '# impeccable\n');
fs.mkdirSync(pluginCacheDir(home, 'marketB', 'impeccable', '2.0.0'), { recursive: true });
fs.writeFileSync(path.join(pluginCacheDir(home, 'marketB', 'impeccable', '2.0.0'), 'SKILL.md'), '# impeccable\n');
try {
const r = run(['probe']);
expect(r.out).toContain(`${SENTINEL.SKILL}: present`);
} finally {
fs.rmSync(path.join(home, '.claude'), { recursive: true, force: true });
}
});
});
describe('scan', () => {
test('not READY → prints the probe lines, exit 0, engine never needed', () => {
const r = run(['scan', 'src/styles.css']);
+14
View File
@@ -28,6 +28,17 @@ function makeTestHome(): string {
return mkdtempSync(join(tmpdir(), "gstack-memory-ingest-"));
}
function isolateGitRemote(repo: string, url: string): void {
const git = (...args: string[]) => {
const result = spawnSync("git", args, { cwd: repo, encoding: "utf8", timeout: 10_000 });
expect(result.status).toBe(0);
return result.stdout.trim();
};
expect(git("config", "--get", "remote.origin.url")).toBe(url);
git("config", "--local", `url.${url}.insteadOf`, url);
expect(git("remote", "get-url", "origin")).toBe(url);
}
function runScript(args: string[], env: Record<string, string> = {}): { stdout: string; stderr: string; exitCode: number } {
const result = spawnSync("bun", [SCRIPT, ...args], {
encoding: "utf-8",
@@ -918,6 +929,7 @@ describe("#2394: probe applies the same attribution gate as prepare", () => {
mkdirSync(repo, { recursive: true });
spawnSync("git", ["-C", repo, "init", "-q"], { encoding: "utf-8", timeout: 30_000 });
spawnSync("git", ["-C", repo, "remote", "add", "origin", "https://github.com/foo/bar.git"], { encoding: "utf-8", timeout: 30_000 });
isolateGitRemote(repo, "https://github.com/foo/bar.git");
return repo;
}
@@ -998,6 +1010,7 @@ describe("#2394: probe applies the same attribution gate as prepare", () => {
mkdirSync(attributableCwd, { recursive: true });
spawnSync("git", ["-C", attributableCwd, "init", "-q"], { encoding: "utf-8", timeout: 30_000 });
spawnSync("git", ["-C", attributableCwd, "remote", "add", "origin", "https://github.com/foo/bar.git"], { encoding: "utf-8", timeout: 30_000 });
isolateGitRemote(attributableCwd, "https://github.com/foo/bar.git");
const ts = new Date().toISOString();
const cwdLine = `{"type":"user","message":{"role":"user","content":"hello"},"timestamp":"${ts}","cwd":"${attributableCwd.replace(/\\/g, "\\\\")}"}\n`;
@@ -1082,6 +1095,7 @@ describe("#2392: transcript ingest honors per-remote trust policy", () => {
mkdirSync(repo, { recursive: true });
spawnSync("git", ["-C", repo, "init", "-q"], { encoding: "utf-8", timeout: 30_000 });
spawnSync("git", ["-C", repo, "remote", "add", "origin", remoteUrl], { encoding: "utf-8", timeout: 30_000 });
isolateGitRemote(repo, remoteUrl);
return repo;
}
+2
View File
@@ -1135,6 +1135,7 @@ export const E2E_TOUCHFILES: Record<string, string[]> = {
'design-review-detector-shim-dom': ['test/session-runner-stream-lifecycle.test.ts', 'design-review/**', 'scripts/resolvers/design.ts', 'lib/design-detect-contract.ts', 'lib/dom-dump-script.ts', 'lib/dom-dump.js', 'bin/gstack-design-detect.ts', 'browse/src/**', 'test/helpers/fake-impeccable.ts', 'test/fixtures/fake-impeccable.ts', 'test/fixtures/impeccable-detect-sample.json', 'test/fixtures/review-eval-design-slop.*', 'test/skill-e2e-design.test.ts',
'scripts/resolvers/testing.ts'
],
'design-review-plugin-handoff': ['test/session-runner-stream-lifecycle.test.ts', 'design-review/**', 'scripts/resolvers/design.ts', 'scripts/resolvers/testing.ts', 'lib/design-catalog.ts', 'lib/design-detect-contract.ts', 'bin/gstack-design-detect.ts', 'test/helpers/hermetic-env.ts', 'test/helpers/fake-impeccable.ts', 'test/fixtures/fake-impeccable.ts', 'test/fixtures/impeccable-detect-sample.json', 'test/fixtures/review-eval-design-slop.html', 'test/skill-e2e-design.test.ts'],
'design-html-slop-gate': ['test/session-runner-stream-lifecycle.test.ts', 'test/gstack-paths.test.ts', 'design-html/**', 'scripts/resolvers/design.ts', 'lib/design-detect-contract.ts', 'bin/gstack-design-detect.ts', 'test/helpers/fake-impeccable.ts', 'test/fixtures/fake-impeccable.ts', 'test/fixtures/impeccable-detect-sample.json', 'test/skill-e2e-design.test.ts'],
// /diagram (diagram-render bundle consumers). Triplet = deterministic
@@ -1677,6 +1678,7 @@ export const E2E_TIERS: Record<string, 'gate' | 'periodic'> = {
'design-review-fix': 'periodic',
'design-review-detector-shim': 'gate', // deterministic sentinels from the fake engine (source mode on a diff)
'design-review-detector-shim-dom': 'gate', // same shim, DOM mode through the browse binary's dump; self-skips when the binary is absent
'design-review-plugin-handoff': 'gate',
'design-html-slop-gate': 'periodic', // one-pass gate behavior is a judgment call on a fake engine's fixed output
// /diagram — triplet is deterministic functional (gstack-render falls back
+15
View File
@@ -49,6 +49,17 @@ const FAKE_AWS_KEY = ['AKIA', '1234567890ABCDEF'].join('');
const CONFIG = path.join(ROOT, 'bin', 'gstack-config');
const POLICY = path.join(ROOT, 'bin', 'gstack-gbrain-repo-policy');
function isolateGitRemote(repo: string, url: string): void {
const git = (...args: string[]) => {
const result = spawnSync('git', args, { cwd: repo, encoding: 'utf8', timeout: 10_000 });
expect(result.status).toBe(0);
return result.stdout.trim();
};
expect(git('config', '--get', 'remote.origin.url')).toBe(url);
git('config', '--local', `url.${url}.insteadOf`, url);
expect(git('remote', 'get-url', 'origin')).toBe(url);
}
const FAKE = `#!/bin/sh
MODE=$(cat "$HOME/mode" 2>/dev/null || echo ok)
printf '%s\\n' "$*" >> "$HOME/calls.log"
@@ -213,6 +224,7 @@ describe('gate on: the mediated hand-off', () => {
const git = (args: string[]) => spawnSync('git', args, { cwd: repo, encoding: 'utf8', timeout: 10_000 });
git(['init', '-q']);
git(['remote', 'add', 'origin', 'https://github.com/example/denied-repo.git']);
isolateGitRemote(repo, 'https://github.com/example/denied-repo.git');
const prompt = JSON.stringify({ prompt: 'hello', cwd: repo });
for (const tier of ['deny', 'read-only']) {
const set = spawnSync('bash', [POLICY, 'set', 'https://github.com/example/denied-repo.git', tier], { env, encoding: 'utf8', timeout: 20_000 });
@@ -693,6 +705,7 @@ describe('deadline and policy failure paths (review coverage)', () => {
const git = (args: string[]) => spawnSync('git', args, { cwd: repo, encoding: 'utf8', timeout: 10_000 });
git(['init', '-q']);
git(['remote', 'add', 'origin', 'https://github.com/example/some-repo.git']);
isolateGitRemote(repo, 'https://github.com/example/some-repo.git');
// a directory where the store file should be: hasRepoPolicyStore() is true, every read fails
const storeDir = path.join(home, '.gstack', 'gbrain-repo-policy.json');
fs.mkdirSync(storeDir, { recursive: true });
@@ -742,6 +755,7 @@ describe('trust-policy lookup outcomes (review coverage, second pass)', () => {
try {
spawnSync('git', ['init', '-q'], { cwd: repo, timeout: 10_000 });
spawnSync('git', ['remote', 'add', 'origin', 'https://github.com/example/other.git'], { cwd: repo, timeout: 10_000 });
isolateGitRemote(repo, 'https://github.com/example/other.git');
expect(runHook(JSON.stringify({ prompt: 'hello', cwd: repo }), {}, repo).stdout).toContain('remembered');
} finally {
fs.rmSync(repo, { recursive: true, force: true });
@@ -755,6 +769,7 @@ describe('trust-policy lookup outcomes (review coverage, second pass)', () => {
for (const [dir, url] of [[denied, 'https://github.com/example/denied.git'], [allowed, 'https://github.com/example/allowed.git']] as const) {
spawnSync('git', ['init', '-q'], { cwd: dir, timeout: 10_000 });
spawnSync('git', ['remote', 'add', 'origin', url], { cwd: dir, timeout: 10_000 });
isolateGitRemote(dir, url);
}
expect(spawnSync('bash', [POLICY, 'set', 'https://github.com/example/denied.git', 'deny'], { env, encoding: 'utf8', timeout: 20_000 }).status).toBe(0);
const r = runHook(JSON.stringify({ prompt: 'hello', cwd: denied }), { GIT_DIR: path.join(allowed, '.git'), GIT_WORK_TREE: allowed }, denied);
+1 -1
View File
@@ -828,7 +828,7 @@ test('stderr lifecycle regression selects runtime consumers without a quality-ma
'outside-voice-codex-to-claude-code', 'outside-voice-claude-code-to-codex', 'outside-plan-disabled-no-fallback', 'ship-coverage-audit', 'review-coverage-audit',
'plan-eng-coverage-audit', 'ship-triage', 'ship-docsync', 'docsync-spawned', 'design-consultation-core',
'design-consultation-existing', 'design-consultation-research', 'design-consultation-preview', 'plan-design-review-no-ui-scope', 'design-review-fix',
'design-review-detector-shim', 'design-review-detector-shim-dom', 'design-html-slop-gate', 'diagram-triplet', 'diagram-authoring-quality',
'design-review-detector-shim', 'design-review-detector-shim-dom', 'design-review-plugin-handoff', 'design-html-slop-gate', 'diagram-triplet', 'diagram-authoring-quality',
'gstack-upgrade-happy-path', 'land-and-deploy-workflow', 'land-and-deploy-first-run', 'land-and-deploy-review-gate', 'canary-workflow',
'benchmark-workflow', 'setup-deploy-workflow', 'autoplan-dual-voice', 'scrape-match-path', 'scrape-prototype-path',
'skillify-happy-path', 'skillify-provenance-refusal', 'skillify-approval-reject', 'journey-ideation', 'journey-plan-eng',
+158
View File
@@ -13,6 +13,7 @@ import {
} from './helpers/e2e-helpers';
import { asideAvailable } from './helpers/aside-available';
import { installFakeImpeccable, DETECT_SAMPLE } from './helpers/fake-impeccable';
import { hermeticChildEnv } from './helpers/hermetic-env';
import { sliceBetween, extractDesignResearchContract } from './helpers/skill-fixture';
import { spawnSync } from 'child_process';
import * as fs from 'fs';
@@ -771,6 +772,163 @@ function makeFakeEngine(): string {
return installFakeImpeccable('skill-e2e-fake-impeccable-').dir;
}
function detectorReportEntries(report: string): string[] {
return report.split(/(?=^[\t ]*(?:#{1,6}\s+|[-*|]\s*|\d+[.)]\s+)?(?:\*\*|`)?FINDING-\d+)/m);
}
if (!evalsEnabled) test('detector report handoffs stay with their entry across inline cross-references', () => {
const report = `### FINDING-001 \`[low-contrast]\` — impact=high — DEFERRED
handoff=\`/impeccable colorize\`
### FINDING-002 \`[ai-color-palette]\` — impact=medium — DEFERRED
The colors also appear in FINDING-001. This is unconfirmed static evidence.
handoff=\`/impeccable colorize\`
### FINDING-003 \`[skipped-heading]\` — impact=medium — DEFERRED
handoff=\`/impeccable typeset\`
`;
const hasPaletteHandoff = (text: string) => detectorReportEntries(text).some(entry => entry.includes('[ai-color-palette]') && entry.includes('/impeccable colorize'));
expect(hasPaletteHandoff(report)).toBe(true);
expect(hasPaletteHandoff(report.replace('handoff=`/impeccable colorize`\n\n### FINDING-003', '### FINDING-003'))).toBe(false);
for (const marker of ['.', ')']) {
const numbered = report.replace(/^### FINDING-(\d+)/gm, (_, number) => `${Number(number)}${marker} **FINDING-${number}**`);
expect(hasPaletteHandoff(numbered)).toBe(true);
expect(hasPaletteHandoff(numbered.replace(`handoff=\`/impeccable colorize\`\n\n3${marker}`, `3${marker}`))).toBe(false);
}
});
function pluginDetectorFixture() {
const fixture = installFakeImpeccable('skill-e2e-plugin-');
const repoDir = path.join(fixture.dir, 'repo');
const home = path.join(fixture.dir, 'home');
const configDir = path.join(fixture.dir, 'custom-claude');
const gstackHome = path.join(fixture.dir, 'gstack');
const impeccableHome = path.join(fixture.dir, 'engine-cache');
for (const dir of [repoDir, home, configDir, gstackHome, impeccableHome]) fs.mkdirSync(dir);
const env = hermeticChildEnv({
HOME: home, USERPROFILE: home, CLAUDE_CONFIG_DIR: configDir,
GSTACK_HOME: gstackHome, IMPECCABLE_HOME: impeccableHome,
CLAUDE_PLUGIN_DATA: '', GSTACK_HEADLESS: '1',
});
for (const key of Object.keys(env)) if (key.startsWith('IMPECCABLE_') && key !== 'IMPECCABLE_HOME') delete env[key];
const engines: Record<string, string> = {};
for (const version of ['4.3.1', '4.10.0']) {
const skillDir = path.join(configDir, 'plugins/cache/fixture-market/impeccable', version, 'skills/impeccable');
const scripts = path.join(skillDir, 'scripts');
const engineDir = path.join(scripts, 'bin', `${process.platform}-${process.arch}`);
fs.mkdirSync(engineDir, { recursive: true });
fs.writeFileSync(path.join(skillDir, 'SKILL.md'), '# Impeccable fixture marker\n');
fs.writeFileSync(path.join(scripts, 'impeccable'), `#!/bin/sh\nprintf launcher > '${fixture.dir}/launcher-ran'\nexit 99\n`, { mode: 0o755 });
fs.writeFileSync(path.join(scripts, 'VERSION'), '1.6.0\n');
engines[version] = path.join(engineDir, 'impeccable');
fs.writeFileSync(engines[version], `#!/bin/sh\nIMPECCABLE_FAKE_LOG='${fixture.dir}/${version}.jsonl' exec '${process.execPath}' '${fixture.bin}' "$@"\n`, { mode: 0o755 });
engines[version] = fs.realpathSync(engines[version]);
}
const git = (...args: string[]) => {
const result = spawnSync('git', args, { cwd: repoDir, env, encoding: 'utf-8', timeout: 5000 });
if (result.status !== 0) throw new Error(`Plugin fixture git ${args[0]} failed: ${result.stderr}`);
};
git('init', '-b', 'main');
git('config', 'user.email', 'test@test.com');
git('config', 'user.name', 'Test');
fs.writeFileSync(path.join(repoDir, 'index.html'), '<h1>Clean</h1>\n');
git('add', '.');
git('commit', '-m', 'initial');
git('checkout', '-b', 'feature/landing');
fs.copyFileSync(path.join(ROOT, 'test/fixtures/review-eval-design-slop.html'), path.join(repoDir, 'index.html'));
git('add', '.');
git('commit', '-m', 'landing page');
fs.writeFileSync(path.join(repoDir, 'design-review-detector.md'), detectorSkillText([
['**Design detector (optional, deterministic):**', '**Create output directories:**'],
['**Phase 0: mechanical scan**', '## Phases 1-6'],
]));
return { dir: fixture.dir, repoDir, env, engines };
}
if (!evalsEnabled) test('plugin detector fixture discovers the selected engine without an override', () => {
const fixture = pluginDetectorFixture();
try {
expect(fixture.env.IMPECCABLE_BIN).toBeUndefined();
const probe = spawnSync(process.execPath, ['--no-env-file', 'run', path.join(ROOT, 'bin/gstack-design-detect.ts'), 'probe', '--host', 'claude'], {
cwd: fixture.repoDir, env: fixture.env, encoding: 'utf-8', timeout: 10000,
});
expect(probe.status).toBe(0);
expect(probe.stdout).toContain(`IMPECCABLE_READY: ${fixture.engines['4.10.0']}`);
expect(probe.stdout).toContain('IMPECCABLE_SKILL: present');
const scan = spawnSync(process.execPath, ['--no-env-file', 'run', path.join(ROOT, 'bin/gstack-design-detect.ts'), 'scan', '--changed', 'main', '--host', 'claude'], {
cwd: fixture.repoDir, env: fixture.env, encoding: 'utf-8', timeout: 10000,
});
expect(scan.status).toBe(2);
expect(scan.stderr).toContain('handoff=/impeccable colorize');
expect(fs.existsSync(path.join(fixture.dir, '4.10.0.jsonl'))).toBe(true);
expect(fs.existsSync(path.join(fixture.dir, '4.3.1.jsonl'))).toBe(false);
expect(fs.existsSync(path.join(fixture.dir, 'launcher-ran'))).toBe(false);
} finally {
fs.rmSync(fixture.dir, { recursive: true, force: true });
}
});
describeIfSelected('Design review plugin discovery E2E', ['design-review-plugin-handoff'], () => {
testConcurrentIfSelected('design-review-plugin-handoff', async () => {
const fixture = pluginDetectorFixture();
try {
const result = await runSkillTest({
prompt: `Load gstack's /design-review workflow by reading design-review-detector.md, the actual Setup detector and Phase 0 excerpt.
Supported actor scope: read that excerpt, run its detector probe and one source-mode scan, then write detector-output.md. This isolated repository is on feature/landing; its base is main. There is no URL.
Do not install or download anything, execute a launcher, change environment variables, browse, ask questions, spawn agents, or edit anything except detector-output.md. Do not read Impeccable skill files. If discovery fails, report that failure and stop; do not repair the environment.
Write the probe's first line and skill-presence line, then one FINDING-NNN entry per DETECT_TOP rule with its [rule-id] and impact. All findings are deferred, unconfirmed static evidence because rendered-page confirmation is outside this actor's scope. Apply the excerpt's deferred-finding reporting requirements. Do not fix source files or claim visual verification.`,
workingDirectory: fixture.repoDir,
maxTurns: 10,
timeout: CAPTURE_MS,
testName: 'design-review-plugin-handoff',
runId,
tools: ['Bash', 'Read', 'Write'],
env: fixture.env,
});
logCost('/design-review plugin handoff', result);
const reportPath = path.join(fixture.repoDir, 'detector-output.md');
const report = fs.existsSync(reportPath) ? fs.readFileSync(reportPath, 'utf-8') : '';
const outputs = result.toolCalls.map(call => String(call.output ?? '')).join('\n');
const commands = result.toolCalls.filter(call => call.tool === 'Bash').map(call => String(call.input?.command ?? ''));
const handoffs = [...outputs.matchAll(/\[([\w-]+)\] impact=(\w+)[^\n]*handoff=(\/impeccable \w+)/g)];
const entries = detectorReportEntries(report);
const engineLog = path.join(fixture.dir, '4.10.0.jsonl');
const invocations = fs.existsSync(engineLog) ? fs.readFileSync(engineLog, 'utf-8').trim().split('\n') : [];
const sourceUnchanged = fs.readFileSync(path.join(fixture.repoDir, 'index.html'), 'utf-8') === fs.readFileSync(path.join(ROOT, 'test/fixtures/review-eval-design-slop.html'), 'utf-8');
if (process.env.GSTACK_EVAL_DIR) {
const evidenceDir = path.join(process.env.GSTACK_EVAL_DIR, 'plugin-handoff', `${Date.now()}`);
fs.mkdirSync(evidenceDir, { recursive: true, mode: 0o700 });
for (const [name, text] of Object.entries({
'report.md': report,
'skill-excerpt.md': fs.readFileSync(path.join(fixture.repoDir, 'design-review-detector.md'), 'utf-8'),
'engine-invocations.jsonl': invocations.join('\n'),
})) fs.writeFileSync(path.join(evidenceDir, name), text, { mode: 0o600 });
}
const checks = {
success: result.exitReason === 'success',
selectedEngine: outputs.includes(`IMPECCABLE_READY: ${fixture.engines['4.10.0']}`),
skillPresent: outputs.includes('IMPECCABLE_SKILL: present'),
probeReported: report.includes(`IMPECCABLE_READY: ${fixture.engines['4.10.0']}`) && report.includes('IMPECCABLE_SKILL: present'),
probeExecuted: commands.some(command => /gstack-design-detect\.ts probe/.test(command)),
scanExecuted: commands.some(command => /gstack-design-detect\.ts scan --changed main/.test(command)),
noInstallOrOverride: !commands.some(command => /\bnpx\b|gstack-design-detect\.ts install|\b(?:curl|wget|npm install|bun add)\b|IMPECCABLE_BIN\s*=/.test(command)),
oneNewEngineInvocation: invocations.length === 1,
oldEngineNotExecuted: !fs.existsSync(path.join(fixture.dir, '4.3.1.jsonl')),
launcherNotExecuted: !fs.existsSync(path.join(fixture.dir, 'launcher-ran')),
sourceUnchanged,
findingsReported: report.includes('FINDING-001') && report.includes('[ai-color-palette]') && report.includes('[low-contrast]'),
deferred: /deferred/i.test(report),
nonemptyHandoffs: handoffs.length > 0,
perFindingHandoff: handoffs.every(([, rule, impact, command]) => entries.some(entry => entry.includes(`[${rule}]`) && entry.toLowerCase().includes(impact) && entry.includes(command))),
};
recordE2E(evalCollector, '/design-review plugin handoff', 'Design review plugin discovery E2E', result, { passed: Object.values(checks).every(Boolean) });
expect(Object.entries(checks).filter(([, passed]) => !passed).map(([name]) => name)).toEqual([]);
} finally {
fs.rmSync(fixture.dir, { recursive: true, force: true });
}
}, CAPTURE_MS);
});
describeIfSelected('Design review detector shim E2E', ['design-review-detector-shim', 'design-review-detector-shim-dom'], () => {
let repoDir: string;
let engineDir: string;