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 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(); }); 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(), ];