Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
2468a25
refactor: add error-handling utility module with selective catches
garrytan Apr 9, 2026
591cd76
refactor: replace defensive try/catches in server.ts with utilities
garrytan Apr 9, 2026
11e56e9
refactor: extract isProcessAlive and replace try/catches in cli.ts
garrytan Apr 9, 2026
1ea3e44
refactor: remove unnecessary return await in content-security and rea…
garrytan Apr 9, 2026
d2349d1
chore: add slop-scan config to exclude vendor files
garrytan Apr 9, 2026
712af16
refactor: replace empty catches with selective error handling in side…
garrytan Apr 9, 2026
4166a98
refactor: replace empty catches and mark pass-through wrappers in bro…
garrytan Apr 9, 2026
fb24bb4
refactor: selective catches in gstack-global-discover
garrytan Apr 9, 2026
b8c4e70
refactor: selective catches in write-commands, cdp-inspector, meta-co…
garrytan Apr 9, 2026
5f9246a
refactor: selective catches in Chrome extension files
garrytan Apr 9, 2026
6a857d4
fix: restore isProcessAlive boolean semantics, add safeUnlinkQuiet, r…
garrytan Apr 9, 2026
ad38e00
fix: use safeUnlinkQuiet in shutdown and cleanup paths
garrytan Apr 9, 2026
12bf74a
revert: remove brittle string-matching catches and alias comments in …
garrytan Apr 9, 2026
a5c5dc6
revert: remove brittle string-matching catches in extension files
garrytan Apr 9, 2026
4590396
docs: add slop-scan usage guidelines to CLAUDE.md
garrytan Apr 9, 2026
52c73ba
chore: add slop-scan as diagnostic in test suite
garrytan Apr 9, 2026
bc66e35
feat: slop-diff shows only NEW findings introduced on this branch
garrytan Apr 9, 2026
5364def
docs: design doc for slop-scan integration in /review and /ship
garrytan Apr 9, 2026
a302593
Merge remote-tracking branch 'origin/main' into garrytan/slop-reduction
garrytan Apr 9, 2026
e4b80dd
chore: bump version and changelog (v0.16.3.0)
garrytan Apr 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
# Changelog

## [0.16.3.0] - 2026-04-09

### Changed
- **AI slop cleanup.** Ran [slop-scan](https://github.com/benvinegar/slop-scan) and dropped from 100 findings (2.38 score/file) to 90 findings (1.96 score/file). The good part: `safeUnlink()` and `safeKill()` utilities that catch real bugs (swallowed EPERM in shutdown was a silent data loss risk). `safeUnlinkQuiet()` for cleanup paths where throwing is worse than swallowing. `isProcessAlive()` extracted to a shared module with Windows support. Redundant `return await` removed. Typed exception catches (TypeError, DOMException, ENOENT) replace empty catches in system boundary code. The part we tried and reverted: string-matching on error messages was brittle, extension catch-and-log was correct as-is, pass-through wrapper comments were linter gaming. We are AI-coded and proud of it. The goal is code quality, not hiding.

### Added
- **`bun run slop:diff`** shows only NEW slop-scan findings introduced on your branch vs main. Line-number-insensitive comparison so shifted code doesn't create false positives. Runs automatically after `bun test`.
- **Slop-scan usage guidelines** in CLAUDE.md: what to fix (genuine quality) vs what NOT to fix (linter gaming). Includes utility function reference table.
- **Design doc** for future slop-scan integration in `/review` and `/ship` skills (`docs/designs/SLOP_SCAN_FOR_REVIEW_SHIP.md`).

## [0.16.2.0] - 2026-04-09

### Added
Expand Down
58 changes: 58 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ bun run dev:skill # watch mode: auto-regen + validate on change
bun run eval:list # list all eval runs from ~/.gstack-dev/evals/
bun run eval:compare # compare two eval runs (auto-picks most recent)
bun run eval:summary # aggregate stats across all eval runs
bun run slop # full slop-scan report (all files)
bun run slop:diff # slop findings in files changed on this branch only
```

`test:evals` requires `ANTHROPIC_API_KEY`. Codex E2E tests (`test/codex-e2e.test.ts`)
Expand Down Expand Up @@ -250,6 +252,62 @@ Examples of good bisection:
When the user says "bisect commit" or "bisect and push," split staged/unstaged
changes into logical commits and push.

## Slop-scan: AI code quality, not AI code hiding

We use [slop-scan](https://github.com/benvinegar/slop-scan) to catch patterns where
AI-generated code is genuinely worse than what a human would write. We are NOT trying
to pass as human code. We are AI-coded and proud of it. The goal is code quality.

```bash
npx slop-scan scan . # human-readable report
npx slop-scan scan . --json # machine-readable for diffing
```

Config: `slop-scan.config.json` at repo root (currently excludes `**/vendor/**`).

### What to fix (genuine quality improvements)

- **Empty catches around file ops** — use `safeUnlink()` (ignores ENOENT, rethrows
EPERM/EIO). A swallowed EPERM in cleanup means silent data loss.
- **Empty catches around process kills** — use `safeKill()` (ignores ESRCH, rethrows
EPERM). A swallowed EPERM means you think you killed something you didn't.
- **Redundant `return await`** — remove when there's no enclosing try block. Saves a
microtask, signals intent.
- **Typed exception catches** — `catch (err) { if (!(err instanceof TypeError)) throw err }`
is genuinely better than `catch {}` when the try block does URL parsing or DOM work.
You know what error you expect, so say so.

### What NOT to fix (linter gaming, not quality)

- **String-matching on error messages** — `err.message.includes('closed')` is brittle.
Playwright/Chrome can change wording anytime. If a fire-and-forget operation can fail
for ANY reason and you don't care, `catch {}` is the correct pattern.
- **Adding comments to exempt pass-through wrappers** — "alias for active session" above
a method just to trip slop-scan's exemption rule is noise, not documentation.
- **Converting extension catch-and-log to selective rethrow** — Chrome extensions crash
entirely on uncaught errors. If the catch logs and continues, that IS the right pattern
for extension code. Don't make it throw.
- **Tightening best-effort cleanup paths** — shutdown, emergency cleanup, and disconnect
code should use `safeUnlinkQuiet()` (swallows ALL errors). A cleanup path that throws
on EPERM means the rest of cleanup doesn't run. That's worse.

### Utilities in `browse/src/error-handling.ts`

| Function | Use when | Behavior |
|----------|----------|----------|
| `safeUnlink(path)` | Normal file deletion | Ignores ENOENT, rethrows others |
| `safeUnlinkQuiet(path)` | Shutdown/emergency cleanup | Swallows all errors |
| `safeKill(pid, signal)` | Sending signals | Ignores ESRCH, rethrows others |
| `isProcessAlive(pid)` | Boolean process checks | Returns true/false, never throws |

### Score tracking

Baseline (2026-04-09, before cleanup): 100 findings, 432.8 score, 2.38 score/file.
After cleanup: 90 findings, 358.1 score, 1.96 score/file.

Don't chase the number. Fix patterns that represent actual code quality problems.
Accept findings where the "sloppy" pattern is the correct engineering choice.

## Community PR guardrails

When reviewing or merging community PRs, **always AskUserQuestion** before accepting
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.16.2.0
0.16.3.0
32 changes: 20 additions & 12 deletions bin/gstack-global-discover.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,8 +167,11 @@ function getGitRemote(cwd: string): string | null {
stdio: ["pipe", "pipe", "pipe"],
}).trim();
return remote || null;
} catch {
return null;
} catch (err: any) {
// Expected: no remote configured, repo not found, git not installed
if (err?.status !== undefined) return null; // non-zero exit from git
if (err?.code === 'ENOENT') return null; // git binary not found
throw err;
}
}

Expand All @@ -183,8 +186,9 @@ function scanClaudeCode(since: Date): Session[] {
let dirs: string[];
try {
dirs = readdirSync(projectsDir);
} catch {
return [];
} catch (err: any) {
if (err?.code === 'ENOENT' || err?.code === 'EACCES') return [];
throw err;
}

for (const dirName of dirs) {
Expand All @@ -209,8 +213,9 @@ function scanClaudeCode(since: Date): Session[] {
const hasRecentFile = jsonlFiles.some((f) => {
try {
return statSync(join(dirPath, f)).mtime >= since;
} catch {
return false;
} catch (err: any) {
if (err?.code === 'ENOENT' || err?.code === 'EACCES') return false;
throw err;
}
});
if (!hasRecentFile) continue;
Expand All @@ -223,8 +228,9 @@ function scanClaudeCode(since: Date): Session[] {
const recentFiles = jsonlFiles.filter((f) => {
try {
return statSync(join(dirPath, f)).mtime >= since;
} catch {
return false;
} catch (err: any) {
if (err?.code === 'ENOENT' || err?.code === 'EACCES') return false;
throw err;
}
});
for (let i = 0; i < recentFiles.length; i++) {
Expand All @@ -251,8 +257,9 @@ function resolveClaudeCodeCwd(
.map((f) => {
try {
return { name: f, mtime: statSync(join(dirPath, f)).mtime.getTime() };
} catch {
return null;
} catch (err: any) {
if (err?.code === 'ENOENT' || err?.code === 'EACCES') return null;
throw err;
}
})
.filter(Boolean)
Expand Down Expand Up @@ -381,8 +388,9 @@ function scanGemini(since: Date): Session[] {
let projectDirs: string[];
try {
projectDirs = readdirSync(tmpDir);
} catch {
return [];
} catch (err: any) {
if (err?.code === 'ENOENT' || err?.code === 'EACCES') return [];
throw err;
}

for (const projectName of projectDirs) {
Expand Down
35 changes: 26 additions & 9 deletions browse/src/browser-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,9 @@ export class BrowserManager {
if (fs.existsSync(path.join(candidate, 'manifest.json'))) {
return candidate;
}
} catch {}
} catch (err: any) {
if (err?.code !== 'ENOENT' && err?.code !== 'EACCES') throw err;
}
}
return null;
}
Expand Down Expand Up @@ -288,11 +290,16 @@ export class BrowserManager {
let origIcon = iconMatch ? iconMatch[1] : 'app';
if (!origIcon.endsWith('.icns')) origIcon += '.icns';
const destIcon = path.join(chromeResources, origIcon);
try { fs.copyFileSync(iconSrc, destIcon); } catch { /* non-fatal */ }
try {
fs.copyFileSync(iconSrc, destIcon);
} catch (err: any) {
if (err?.code !== 'ENOENT' && err?.code !== 'EACCES') throw err;
}
}
}
} catch {
// Non-fatal: app name just stays as Chrome for Testing
} catch (err: any) {
// Non-fatal: app name stays as Chrome for Testing (ENOENT/EACCES expected)
if (err?.code !== 'ENOENT' && err?.code !== 'EACCES') throw err;
}

// Build custom user agent: keep Chrome version for site compatibility,
Expand Down Expand Up @@ -364,7 +371,11 @@ export class BrowserManager {
const cleanup = () => {
for (const key of Object.keys(window)) {
if (key.startsWith('cdc_') || key.startsWith('__webdriver')) {
try { delete (window as any)[key]; } catch {}
try {
delete (window as any)[key];
} catch (e: any) {
if (!(e instanceof TypeError)) throw e;
}
}
}
};
Expand Down Expand Up @@ -446,7 +457,9 @@ export class BrowserManager {
this.activeTabId = id;
this.wirePageEvents(page);
// Inject indicator on restored page (addInitScript only fires on new navigations)
try { await page.evaluate(indicatorScript); } catch {}
try {
await page.evaluate(indicatorScript);
} catch {}
} else {
await this.newTab();
}
Expand Down Expand Up @@ -581,7 +594,9 @@ export class BrowserManager {
try {
const u = new URL(activeUrl);
activeOriginPath = u.origin + u.pathname;
} catch {}
} catch (err: any) {
if (!(err instanceof TypeError)) throw err;
}

for (const [id, page] of this.pages) {
try {
Expand All @@ -598,7 +613,9 @@ export class BrowserManager {
if (pu.origin + pu.pathname === activeOriginPath) {
fuzzyId = id;
}
} catch {}
} catch (err: any) {
if (!(err instanceof TypeError)) throw err;
}
}
} catch {}
}
Expand Down Expand Up @@ -1131,7 +1148,7 @@ export class BrowserManager {
await dialog.dismiss();
}
} catch {
// Dialog may have been dismissed by navigation — ignore
// Dialog may have been dismissed by navigation
}
});

Expand Down
59 changes: 25 additions & 34 deletions browse/src/cdp-inspector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,9 @@ async function getOrCreateSession(page: Page): Promise<any> {
try {
await session.send('DOM.getDocument', { depth: 0 });
return session;
} catch {
// Session is stale — recreate
} catch (err: any) {
// Session is stale — recreate (CDP disconnects throw on closed/Target errors)
if (!err?.message?.includes('closed') && !err?.message?.includes('Target') && !err?.message?.includes('detached')) throw err;
cdpSessions.delete(page);
initializedPages.delete(page);
}
Expand All @@ -117,7 +118,9 @@ async function getOrCreateSession(page: Page): Promise<any> {
page.once('framenavigated', () => {
try {
session.detach().catch(() => {});
} catch {}
} catch (err: any) {
if (!err?.message?.includes('closed') && !err?.message?.includes('Target') && !err?.message?.includes('detached')) throw err;
}
cdpSessions.delete(page);
initializedPages.delete(page);
});
Expand Down Expand Up @@ -258,8 +261,9 @@ export async function inspectElement(
left: border[0] - margin[0],
},
};
} catch {
// Element may not have a box model (e.g., display:none)
} catch (err: any) {
// Element may not have a box model (e.g., display:none) — CDP returns "Could not compute box model"
if (!err?.message?.includes('box model') && !err?.message?.includes('Could not compute')) throw err;
}

// Get matched styles
Expand Down Expand Up @@ -315,10 +319,8 @@ export async function inspectElement(

if (rule.styleSheetId) {
styleSheetId = rule.styleSheetId;
try {
// Try to resolve stylesheet URL
source = rule.origin === 'regular' ? (rule.styleSheetId || 'stylesheet') : rule.origin;
} catch {}
// Resolve stylesheet source name
source = rule.origin === 'regular' ? (rule.styleSheetId || 'stylesheet') : rule.origin;
}

if (rule.style?.range) {
Expand All @@ -328,15 +330,7 @@ export async function inspectElement(
}

// Try to get a friendly source name from stylesheet
if (styleSheetId) {
try {
// Stylesheet URL might be embedded in the rule data
// CDP provides sourceURL in some cases
if (rule.style?.cssText) {
// Parse source from the styleSheetId metadata
}
} catch {}
}
// (styleSheetId metadata is available via CDP — see stylesheet URL resolution below)

// Get media query if present
let media: string | undefined;
Expand Down Expand Up @@ -433,15 +427,9 @@ export async function inspectElement(
}

// Resolve stylesheet URLs for better source info
for (const rule of matchedRules) {
if (rule.styleSheetId && rule.source !== 'inline') {
try {
const sheetMeta = await session.send('CSS.getStyleSheetText', { styleSheetId: rule.styleSheetId }).catch(() => null);
// Try to get the stylesheet header for URL info
// The styleSheetId itself is opaque, but we can try to get source URL
} catch {}
}
}
// Note: CSS.getStyleSheetText is called per-rule but result is unused — the styleSheetId
// is opaque and CDP doesn't expose a direct URL lookup. Left as a placeholder for future
// enhancement (e.g., CSS.styleSheetAdded event tracking).

return {
selector,
Expand Down Expand Up @@ -531,8 +519,9 @@ export async function modifyStyle(
method = 'setStyleTexts';
source = `${targetRule.source}:${targetRule.sourceLine}`;
sourceLine = targetRule.sourceLine;
} catch {
// Fall back to inline
} catch (err: any) {
// Fall back to inline — setStyleTexts fails on immutable stylesheets or stale ranges
if (!err?.message?.includes('style') && !err?.message?.includes('range') && !err?.message?.includes('closed') && !err?.message?.includes('Target')) throw err;
}
}

Expand Down Expand Up @@ -591,8 +580,9 @@ export async function undoModification(page: Page, index?: number): Promise<void
await modifyStyle(page, mod.selector, mod.property, mod.oldValue);
// Remove the undo modification from history (it's a restore, not a new mod)
modificationHistory.pop();
} catch {
// Fall back to inline restore
} catch (err: any) {
// Fall back to inline restore — CDP may have disconnected or stylesheet changed
if (!err?.message?.includes('closed') && !err?.message?.includes('Target') && !err?.message?.includes('style') && !err?.message?.includes('not found') && !err?.message?.includes('Element')) throw err;
await page.evaluate(
([sel, prop, val]) => {
const el = document.querySelector(sel);
Expand Down Expand Up @@ -652,8 +642,9 @@ export async function resetModifications(page: Page): Promise<void> {
},
[mod.selector, mod.property, mod.oldValue]
);
} catch {
// Best effort
} catch (err: any) {
// Best effort — page may have navigated or element may be gone
if (!err?.message?.includes('closed') && !err?.message?.includes('Target') && !err?.message?.includes('Execution context')) throw err;
}
}
modificationHistory.length = 0;
Expand Down Expand Up @@ -757,7 +748,7 @@ export function detachSession(page?: Page): void {
if (page) {
const session = cdpSessions.get(page);
if (session) {
try { session.detach().catch(() => {}); } catch {}
try { session.detach().catch(() => {}); } catch (err: any) { if (!err?.message?.includes('closed') && !err?.message?.includes('Target') && !err?.message?.includes('detached')) throw err; }
cdpSessions.delete(page);
initializedPages.delete(page);
}
Expand Down
Loading
Loading