fix(ship): return commitSha + coolifyDeployUrl, prevent verification chain

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
This commit is contained in:
2026-04-28 14:46:18 -07:00
parent e0844b5f2e
commit a897d07179
3 changed files with 47 additions and 8 deletions

View File

@@ -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() {

View File

@@ -3458,6 +3458,8 @@ async function toolShip(principal: Principal, params: Record<string, any>) {
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.`,
},
});
}

View File

@@ -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: {