From e7bd954263e887e61d809f9d50ba3dccee8609af Mon Sep 17 00:00:00 2001 From: Naveen Chatlapalli Date: Fri, 27 Mar 2026 12:06:10 -0500 Subject: [PATCH 1/3] Add cross-platform URL open helper (gstack-open-url) Adds a new bin/gstack-open-url script that opens URLs in the default browser across macOS, Linux, and Windows platforms. - Uses 'open' on macOS - Uses 'xdg-open' on Linux - Uses 'start' on Windows - Supports GSTACK_OPEN_CMD env override for custom handlers This fixes the hardcoded 'open' command that only worked on macOS, enabling the Boil the Lake and Search Before Building intro flows to work on all supported platforms. Closes TODOS.md item: Cross-platform URL open helper --- bin/gstack-open-url | 49 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 bin/gstack-open-url diff --git a/bin/gstack-open-url b/bin/gstack-open-url new file mode 100644 index 000000000..68508f861 --- /dev/null +++ b/bin/gstack-open-url @@ -0,0 +1,49 @@ +#!/usr/bin/env bash +# gstack-open-url — cross-platform URL opener +# +# Usage: +# gstack-open-url — open URL in default browser +# +# Supports macOS (open), Linux (xdg-open), and Windows (start) +# +# Env overrides (for testing): +# GSTACK_OPEN_CMD — override the open command +set -euo pipefail + +URL="${1:-}" + +if [ -z "$URL" ]; then + echo "Usage: gstack-open-url " >&2 + exit 1 +fi + +# Allow explicit override via environment +if [ -n "${GSTACK_OPEN_CMD:-}" ]; then + $GSTACK_OPEN_CMD "$URL" + exit 0 +fi + +# Detect platform and use appropriate command +case "$(uname -s)" in + Darwin) + # macOS + open "$URL" + ;; + Linux) + # Linux - use xdg-open + if command -v xdg-open >/dev/null 2>&1; then + xdg-open "$URL" + else + echo "Error: xdg-open not found. Install xdg-utils or set GSTACK_OPEN_CMD." >&2 + exit 1 + fi + ;; + CYGWIN*|MINGW*|MSYS*) + # Windows + start "" "$URL" + ;; + *) + echo "Error: Unknown platform $(uname -s). Set GSTACK_OPEN_CMD to override." >&2 + exit 1 + ;; +esac From 4617323d3bc66fc814c8c919c0d177406fed2a83 Mon Sep 17 00:00:00 2001 From: Naveen Chatlapalli Date: Fri, 27 Mar 2026 12:06:18 -0500 Subject: [PATCH 2/3] Add Search Before Building intro and cross-platform URL support Adds the first-time Search Before Building intro flow, similar to the existing Boil the Lake intro. Changes: - Add generateSearchIntro() function that introduces the Search Before Building principle with a link to the essay - Add SEARCH_INTRO preamble variable that checks for .search-intro-seen - Update generateLakeIntro() to use the new gstack-open-url helper for cross-platform URL opening - Add generateSearchIntro() to tier >= 3 preamble sections This creates a consistent pattern for first-time principle introductions and enables cross-platform browser opening for both intros. Closes TODOS.md item: First-time Search Before Building intro --- scripts/resolvers/preamble.ts | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/scripts/resolvers/preamble.ts b/scripts/resolvers/preamble.ts index 4e5092f8a..f4d6a9969 100644 --- a/scripts/resolvers/preamble.ts +++ b/scripts/resolvers/preamble.ts @@ -33,6 +33,8 @@ REPO_MODE=\${REPO_MODE:-unknown} echo "REPO_MODE: $REPO_MODE" _LAKE_SEEN=$([ -f ~/.gstack/.completeness-intro-seen ] && echo "yes" || echo "no") echo "LAKE_INTRO: $_LAKE_SEEN" +_SEARCH_SEEN=$([ -f ~/.gstack/.search-intro-seen ] && echo "yes" || echo "no") +echo "SEARCH_INTRO: $_SEARCH_SEEN" _TEL=$(${ctx.paths.binDir}/gstack-config get telemetry 2>/dev/null || true) _TEL_PROMPTED=$([ -f ~/.gstack/.telemetry-prompted ] && echo "yes" || echo "no") _TEL_START=$(date +%s) @@ -61,18 +63,33 @@ of \`/qa\`, \`/gstack-ship\` instead of \`/ship\`). Disk paths are unaffected If output shows \`UPGRADE_AVAILABLE \`: read \`${ctx.paths.skillRoot}/gstack-upgrade/SKILL.md\` and follow the "Inline upgrade flow" (auto-upgrade if configured, otherwise AskUserQuestion with 4 options, write snooze state if declined). If \`JUST_UPGRADED \`: tell user "Running gstack v{to} (just updated!)" and continue.`; } -function generateLakeIntro(): string { +function generateLakeIntro(ctx: TemplateContext): string { return `If \`LAKE_INTRO\` is \`no\`: Before continuing, introduce the Completeness Principle. Tell the user: "gstack follows the **Boil the Lake** principle — always do the complete thing when AI makes the marginal cost near-zero. Read more: https://garryslist.org/posts/boil-the-ocean" Then offer to open the essay in their default browser: \`\`\`bash -open https://garryslist.org/posts/boil-the-ocean +${ctx.paths.binDir}/gstack-open-url https://garryslist.org/posts/boil-the-ocean touch ~/.gstack/.completeness-intro-seen \`\`\` -Only run \`open\` if the user says yes. Always run \`touch\` to mark as seen. This only happens once.`; +Only run \`gstack-open-url\` if the user says yes. Always run \`touch\` to mark as seen. This only happens once.`; +} + +function generateSearchIntro(ctx: TemplateContext): string { + return `If \`SEARCH_INTRO\` is \`no\`: Before continuing, introduce the Search Before Building principle. +Tell the user: "gstack follows the **Search Before Building** principle — always search +for existing solutions before building from scratch. When the conventional approach is +wrong for your specific case, that's where brilliance occurs. Read more: https://garryslist.org/posts/search-before-building" +Then offer to open the essay in their default browser: + +\`\`\`bash +${ctx.paths.binDir}/gstack-open-url https://garryslist.org/posts/search-before-building +touch ~/.gstack/.search-intro-seen +\`\`\` + +Only run \`gstack-open-url\` if the user says yes. Always run \`touch\` to mark as seen. This only happens once.`; } function generateTelemetryPrompt(ctx: TemplateContext): string { @@ -477,12 +494,12 @@ export function generatePreamble(ctx: TemplateContext): string { const sections = [ generatePreambleBash(ctx), generateUpgradeCheck(ctx), - generateLakeIntro(), + generateLakeIntro(ctx), generateTelemetryPrompt(ctx), generateProactivePrompt(ctx), generateVoiceDirective(tier), ...(tier >= 2 ? [generateAskUserFormat(ctx), generateCompletenessSection()] : []), - ...(tier >= 3 ? [generateRepoModeSection(), generateSearchBeforeBuildingSection(ctx)] : []), + ...(tier >= 3 ? [generateRepoModeSection(), generateSearchBeforeBuildingSection(ctx), generateSearchIntro(ctx)] : []), generateContributorMode(), generateCompletionStatus(), ]; From b503f9373fb631ef355608139658d6668ae0419a Mon Sep 17 00:00:00 2001 From: Naveen Chatlapalli Date: Fri, 27 Mar 2026 12:06:25 -0500 Subject: [PATCH 3/3] Add Write tool to sidebar agent and improve error visibility Fixes two issues with the sidebar agent: 1. Add Write tool support: - Adds 'Write' to --allowedTools list - Users can now create files (CSVs, logs, etc.) via sidebar agent 2. Improve error visibility: - Capture stderr output instead of ignoring it - Include stderr in error messages for spawn errors and timeouts - This helps debug issues when Claude errors or returns empty output Previously, users asking to write files would silently fail, and errors were invisible. Now both issues are surfaced properly. Closes TODOS.md item: Sidebar agent needs Write tool + better error visibility --- browse/src/sidebar-agent.ts | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/browse/src/sidebar-agent.ts b/browse/src/sidebar-agent.ts index 6eb2cebbb..05fa63d75 100644 --- a/browse/src/sidebar-agent.ts +++ b/browse/src/sidebar-agent.ts @@ -160,7 +160,7 @@ async function askClaude(queueEntry: any): Promise { return new Promise((resolve) => { // Build args fresh — don't trust --resume from queue (session may be stale) let claudeArgs = ['-p', prompt, '--output-format', 'stream-json', '--verbose', - '--allowedTools', 'Bash,Read,Glob,Grep']; + '--allowedTools', 'Bash,Read,Glob,Grep,Write']; // Validate cwd exists — queue may reference a stale worktree let effectiveCwd = cwd || process.cwd(); @@ -186,7 +186,11 @@ async function askClaude(queueEntry: any): Promise { } }); - proc.stderr.on('data', () => {}); // Claude logs to stderr, ignore + // Capture stderr for error reporting + let stderrBuffer = ''; + proc.stderr.on('data', (data: Buffer) => { + stderrBuffer += data.toString(); + }); proc.on('close', (code) => { if (buffer.trim()) { @@ -199,7 +203,8 @@ async function askClaude(queueEntry: any): Promise { }); proc.on('error', (err) => { - sendEvent({ type: 'agent_error', error: err.message }).then(() => { + const errorMsg = stderrBuffer ? `${err.message}\nStderr: ${stderrBuffer}` : err.message; + sendEvent({ type: 'agent_error', error: errorMsg }).then(() => { isProcessing = false; resolve(); }); @@ -209,7 +214,10 @@ async function askClaude(queueEntry: any): Promise { const timeoutMs = parseInt(process.env.SIDEBAR_AGENT_TIMEOUT || '300000', 10); setTimeout(() => { try { proc.kill(); } catch {} - sendEvent({ type: 'agent_error', error: `Timed out after ${timeoutMs / 1000}s` }).then(() => { + const errorMsg = stderrBuffer + ? `Timed out after ${timeoutMs / 1000}s\nStderr: ${stderrBuffer}` + : `Timed out after ${timeoutMs / 1000}s`; + sendEvent({ type: 'agent_error', error: errorMsg }).then(() => { isProcessing = false; resolve(); });