From a897d071790fc60b93af29beb2c82c0b3c96cdda Mon Sep 17 00:00:00 2001 From: Mark Henderson Date: Tue, 28 Apr 2026 14:46:18 -0700 Subject: [PATCH] fix(ship): return commitSha + coolifyDeployUrl, prevent verification chain MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After "ship" succeeded the AI was burning 7+ follow-up tool calls (gitea_repos_list, gitea_credentials, shell.exec×4, apps_list) trying to verify what actually got pushed and where it deployed. That ate through MAX_TOOL_ROUNDS and the user got tool-icon spam with no narrative summary. Three fixes: 1. ship now returns commitSha (parsed from `git rev-parse HEAD`), giteaCommitUrl, giteaBranchUrl, coolifyDeployUrl, coolifyAppUuid, and a summaryHint string telling the AI exactly what to say next. 2. ship's tool description now explicitly tells Gemini "do NOT call gitea_*, shell_exec, or apps_* afterwards to verify — the result is authoritative." 3. MAX_TOOL_ROUNDS 12 → 18 as a safety net for genuinely long chains. Net effect: ship goes from ~12 tool calls to verify a deploy down to 1 (just ship itself), and the next text turn has the SHA + URL inline. Made-with: Cursor --- app/api/chat/route.ts | 2 +- app/api/mcp/route.ts | 48 +++++++++++++++++++++++++++++++++++++------ lib/ai/vibn-tools.ts | 5 ++++- 3 files changed, 47 insertions(+), 8 deletions(-) diff --git a/app/api/chat/route.ts b/app/api/chat/route.ts index 9ae0acdc..a0a19572 100644 --- a/app/api/chat/route.ts +++ b/app/api/chat/route.ts @@ -24,7 +24,7 @@ import type { ChatMessage, ToolCall } from '@/lib/ai/gemini-chat'; // tool calls in one user turn. When the cap IS hit, we still emit a // narrative summary instead of leaving the user staring at a tool tray // (see the no-tools follow-up call below). -const MAX_TOOL_ROUNDS = 12; +const MAX_TOOL_ROUNDS = 18; let chatTablesReady = false; async function ensureChatTables() { diff --git a/app/api/mcp/route.ts b/app/api/mcp/route.ts index ff97d072..8200daa1 100644 --- a/app/api/mcp/route.ts +++ b/app/api/mcp/route.ts @@ -3458,6 +3458,8 @@ async function toolShip(principal: Principal, params: Record) { const apiHost = new URL(GITEA_API_URL).host; const remote = `https://${creds.username}:${creds.token}@${apiHost}/${creds.org}/${repo}.git`; + // Capture the resulting HEAD SHA on stdout in a parseable form so we + // can return it to the caller without a second exec round-trip. const cmd = `set -e cd /workspace if [ ! -d .git ]; then @@ -3474,16 +3476,21 @@ if git diff --cached --quiet HEAD 2>/dev/null; then else git commit -q -m ${shq(message)} fi -git push -u origin HEAD:${shq(branch)} 2>&1 | tail -5`; +git push -u origin HEAD:${shq(branch)} 2>&1 | tail -5 +echo "VIBN_SHIP_SHA=$(git rev-parse HEAD)"`; let pushOutput = ''; + let commitSha: string | null = null; try { const r = await execInDevContainer({ projectId: project.id, command: cmd, timeoutMs: 60_000, }); - pushOutput = (r.stdout + r.stderr).trim(); + const combined = (r.stdout + r.stderr).trim(); + const shaMatch = combined.match(/VIBN_SHIP_SHA=([0-9a-f]{7,40})/); + commitSha = shaMatch ? shaMatch[1] : null; + pushOutput = combined.replace(/VIBN_SHIP_SHA=[0-9a-f]+\s*$/, '').trim(); if (r.code !== 0) { return NextResponse.json( { error: `git push failed: ${pushOutput}` }, @@ -3497,23 +3504,45 @@ git push -u origin HEAD:${shq(branch)} 2>&1 | tail -5`; ); } + // Build verification URLs the AI can hand to the user without + // additional tool calls (the previous behaviour cost ~7 follow-up + // calls per ship: gitea.repos.list, gitea.credentials, multiple + // shell.exec verifications, apps_list). + const giteaWebHost = new URL(GITEA_API_URL).host.replace(/^api\./, ''); + const giteaCommitUrl = commitSha + ? `https://${giteaWebHost}/${creds.org}/${repo}/commit/${commitSha}` + : null; + const giteaCompareUrl = `https://${giteaWebHost}/${creds.org}/${repo}/commits/branch/${branch}`; + // Trigger Coolify deploy if the project is linked to one. let deploymentUuid: string | null = null; + let coolifyDeployUrl: string | null = null; const linkedAppUuid = typeof project.data?.coolifyAppUuid === 'string' && project.data.coolifyAppUuid.trim() ? project.data.coolifyAppUuid.trim() : null; + const coolifyHost = process.env.COOLIFY_BASE_URL + ? new URL(process.env.COOLIFY_BASE_URL).host + : null; if (linkedAppUuid && Boolean(params.deploy ?? true)) { try { const ownedUuids = await getOwnedCoolifyProjectUuids(principal.workspace); await getApplicationInWorkspace(linkedAppUuid, ownedUuids); const dep = await deployApplication(linkedAppUuid, { force: false }); deploymentUuid = dep.deployment_uuid; + if (coolifyHost && deploymentUuid) { + coolifyDeployUrl = `https://${coolifyHost}/project/${linkedAppUuid}/deployments/${deploymentUuid}`; + } } catch (err) { return NextResponse.json({ result: { + repo, + branch, + message, pushed: true, + commitSha, pushOutput, + giteaCommitUrl, deploymentTriggered: false, deployError: err instanceof Error ? err.message : String(err), }, @@ -3527,14 +3556,21 @@ git push -u origin HEAD:${shq(branch)} 2>&1 | tail -5`; branch, message, pushed: true, + commitSha, pushOutput, + giteaCommitUrl, + giteaBranchUrl: giteaCompareUrl, deploymentTriggered: Boolean(deploymentUuid), deploymentUuid, - hint: deploymentUuid - ? 'Deploy in progress; poll apps_deployments to track.' + coolifyDeployUrl, + coolifyAppUuid: linkedAppUuid, + // Tell the AI exactly what to say in the next text turn so it + // doesn't waste tool rounds verifying. + summaryHint: deploymentUuid + ? `Pushed commit ${commitSha?.slice(0, 7) ?? '?'} to ${repo}@${branch} and triggered Coolify deployment ${deploymentUuid}. Show the user commitSha (full or short) and coolifyDeployUrl. Do NOT call additional tools to verify.` : linkedAppUuid - ? 'Deploy was skipped (deploy=false).' - : 'No Coolify app linked to this project yet — call apps_create to wire one up before the next ship.', + ? `Pushed commit ${commitSha?.slice(0, 7) ?? '?'}, deploy was skipped per deploy=false. Show commitSha and giteaCommitUrl.` + : `Pushed commit ${commitSha?.slice(0, 7) ?? '?'}. No Coolify app linked yet — tell the user to call apps_create once before the next ship. Show commitSha and giteaCommitUrl.`, }, }); } diff --git a/lib/ai/vibn-tools.ts b/lib/ai/vibn-tools.ts index 4b97e31e..6fdd1710 100644 --- a/lib/ai/vibn-tools.ts +++ b/lib/ai/vibn-tools.ts @@ -811,7 +811,10 @@ Auto-domain {name}.{workspace}.vibnai.com is assigned automatically.`, description: 'Graduate the project from dev container to production. Commits everything in /workspace, pushes to the project Gitea repo, ' + 'and triggers a Coolify production deploy if the project is linked to one. Use when the user says "ship it", "deploy this", ' + - 'or after a stable working state has been verified via dev_server_*. Pass `commitMsg` for a meaningful commit; otherwise an ISO-timestamp message is used.', + 'or after a stable working state has been verified via dev_server_*. Pass `commitMsg` for a meaningful commit; otherwise an ISO-timestamp message is used. ' + + 'Returns { commitSha, giteaCommitUrl, deploymentUuid, coolifyDeployUrl, summaryHint }. ' + + 'IMPORTANT: do NOT call gitea_*, shell_exec, or apps_* afterwards to verify — the result is authoritative. ' + + 'Just report commitSha + coolifyDeployUrl to the user.', parameters: { type: 'OBJECT', properties: {