fix(ai): implement Phase 2 and 3 prompt recommendations from review

This commit is contained in:
2026-05-19 13:47:18 -07:00
parent 618f7796b2
commit bbcd4ad55e
6 changed files with 857 additions and 22 deletions

View File

@@ -215,6 +215,8 @@ Each project has a persistent \`vibn-dev\` container. Edit files via \`fs_*\` an
**Start a coding session:** \`devcontainer_ensure { projectId }\` (idempotent; first call ~10s, then instant).
**Orient yourself once.** On the first code-modifying turn of a chat, call \`fs_tree\` once to learn the repo layout. Don't re-run it on every turn — the layout doesn't change between user messages.
**Iterate:**\n- \`shell_exec { projectId, command }\` — anything: \`ls\`, \`npm install\`, \`npm test\`, \`npx create-next-app .\`, \`git status\`. Cwd defaults to \`/workspace\`. Node (LTS), Python 3.12, and Go 1.23 are pre-installed — no setup needed.\n- \`fs_read\` / \`fs_write\` / \`fs_edit { path, oldString, newString, startLine, endLine }\`. IMPORTANT: For fs_edit, ALWAYS prefer using \`oldString\` for small replacements if you are confident. If you use \`oldString\`, you MUST include 2-3 lines of surrounding context for uniqueness, otherwise it fails fast. If you are replacing large blocks, use \`startLine\` and \`endLine\` instead.\n- \`fs_glob\` / \`fs_grep\` (ripgrep, respects .gitignore) / \`fs_list\` / \`fs_delete\`.\n
**Dev servers (preview URL via \`*.preview.vibnai.com\` wildcard):**
@@ -250,7 +252,7 @@ For NEW repos / branches: \`gitea_repos_list\`, \`gitea_repo_get\`, \`gitea_repo
## Troubleshooting
- **Dev container stuck provisioning (>120s)**: \`devcontainer_status\` returns \`likelyFailed: true\` and a \`coolifyStatus\` field with Coolify's view. If \`blockedReason\` is set, TELL THE USER the specific reason ("SSH not configured", "Coolify deploy failed: image pull error") instead of continuing to poll. Do NOT loop on \`devcontainer_status\` — a stuck container will NOT self-heal. If the status says "failed" or "error", advise the user to check their Coolify dashboard or regenerate the project.
- "exited (1)" / deploy stuck → \`apps_logs { uuid }\` + \`apps_containers_list { uuid }\`. Usual: missing env, wrong port, image pull fail.
- "exited (1)" / deploy stuck → \`apps_logs { uuid }\` + \`apps_containers_ps { uuid }\`. Usual: missing env, wrong port, image pull fail.
- 502 / "no available server" → \`apps_get\`; if \`fqdn\` is empty, attach a domain.
- "tenant" / "does not belong to" → uuid not in this workspace. Re-list with \`apps_list\`.
- Compose stack weird → \`apps_repair { uuid }\` re-applies Traefik labels + port forwarding.

View File

@@ -4491,28 +4491,44 @@ function shq(s: string): string {
return `'${s.replace(/'/g, `'\\''`)}'`;
}
function normalizeFsPath(p: string): string | NextResponse {
function normalizeFsPath(
p: string,
projectSlug?: string,
): string | NextResponse {
if (!p || typeof p !== "string") {
return NextResponse.json(
{ error: 'Param "path" is required' },
{ status: 400 },
);
}
const projectRoot = projectSlug ? `${FS_ROOT}/${projectSlug}` : FS_ROOT;
let abs: string;
if (p.startsWith("/")) {
abs = p;
} else {
abs = `${FS_ROOT}/${p}`.replace(/\/+/g, "/");
abs = `${projectRoot}/${p}`.replace(/\/+/g, "/");
}
// Disallow .. traversal that escapes /workspace.
const norm = abs.replace(/\/[^/]+\/\.\.(?=\/|$)/g, "").replace(/\/+/g, "/");
if (!norm.startsWith(FS_ROOT) && norm !== FS_ROOT) {
return NextResponse.json(
{
error: `Path "${p}" is outside ${FS_ROOT}; use shell.exec for system paths.`,
},
{ status: 400 },
);
// When projectSlug is set, REJECT paths outside the project root.
if (projectSlug) {
if (!norm.startsWith(projectRoot) && norm !== projectRoot) {
return NextResponse.json(
{
ok: false,
error: `PATH_OUTSIDE_PROJECT: path "${p}" resolves to "${norm}" which is outside the active project at "${projectRoot}". Did you mean "${projectRoot}/${p.replace(/^\/+/, "")}"?`,
},
{ status: 400 },
);
}
} else {
// Workspace-level fallback (legacy behaviour)
if (!norm.startsWith(FS_ROOT) && norm !== FS_ROOT) {
return NextResponse.json(
{ error: `Path "${p}" is outside ${FS_ROOT}; use shell.exec for system paths.` },
{ status: 400 },
);
}
}
return norm;
}
@@ -4627,7 +4643,7 @@ async function toolFsRead(principal: Principal, params: Record<string, any>) {
if (guard) return guard;
const project = await resolveProjectOr404(principal, params);
if (project instanceof NextResponse) return project;
const path = normalizeFsPath(String(params.path ?? ""));
const path = normalizeFsPath(String(params.path ?? ""), project.slug);
if (path instanceof NextResponse) return path;
const offset = Number.isFinite(Number(params.offset))
@@ -4873,7 +4889,7 @@ async function toolFsWrite(principal: Principal, params: Record<string, any>) {
if (guard) return guard;
const project = await resolveProjectOr404(principal, params);
if (project instanceof NextResponse) return project;
const path = normalizeFsPath(String(params.path ?? ""));
const path = normalizeFsPath(String(params.path ?? ""), project.slug);
if (path instanceof NextResponse) return path;
const content = typeof params.content === "string" ? params.content : "";
@@ -4890,8 +4906,11 @@ async function toolFsWrite(principal: Principal, params: Record<string, any>) {
{ status: 500 },
);
}
const { createHash } = require('crypto');
const bytes = Buffer.byteLength(content, "utf8");
const sha256 = createHash("sha256").update(content, "utf8").digest("hex");
return NextResponse.json({
result: { path, bytesWritten: Buffer.byteLength(content, "utf8") },
result: { ok: true, path, bytes, sha256 },
});
}
@@ -4900,7 +4919,7 @@ async function toolFsEdit(principal: Principal, params: Record<string, any>) {
if (guard) return guard;
const project = await resolveProjectOr404(principal, params);
if (project instanceof NextResponse) return project;
const path = normalizeFsPath(String(params.path ?? ""));
const path = normalizeFsPath(String(params.path ?? ""), project.slug);
if (path instanceof NextResponse) return path;
const newString =
@@ -4966,7 +4985,7 @@ print(n)`;
const b64 = Buffer.from(JSON.stringify(payload), "utf8").toString("base64");
const pyB64 = Buffer.from(py, "utf8").toString("base64");
const cmd = `python3 -c "$(printf %s ${shq(pyB64)} | base64 -d)" <<< "$(printf %s ${shq(b64)} | base64 -d)"`;
const cmd = `python3 -c "$(printf %s ${shq(pyB64)} | base64 -d)" <<< "$(printf %s ${shq(b64)} | base64 -d)" && echo "---" && sha256sum ${shq(path)} | cut -d' ' -f1 && wc -c < ${shq(path)}`;
const r = await runFsCmd(principal, project, cmd);
if (r.code !== 0) {
@@ -4979,8 +4998,18 @@ print(n)`;
{ status },
);
}
const stdoutParts = r.stdout.split('---');
const replacementsStr = stdoutParts[0].trim();
const hashAndSize = stdoutParts[1] ? stdoutParts[1].trim().split('\n') : [];
return NextResponse.json({
result: { path, replacements: parseInt(r.stdout.trim() || "0", 10) },
result: {
ok: true,
path,
replacements: parseInt(replacementsStr || "0", 10),
sha256: hashAndSize[0] ? hashAndSize[0].trim() : undefined,
bytes: hashAndSize[1] ? parseInt(hashAndSize[1].trim(), 10) : undefined
},
});
}
@@ -4989,7 +5018,7 @@ async function toolFsList(principal: Principal, params: Record<string, any>) {
if (guard) return guard;
const project = await resolveProjectOr404(principal, params);
if (project instanceof NextResponse) return project;
const path = normalizeFsPath(String(params.path ?? "/workspace"));
const path = normalizeFsPath(String(params.path ?? "/workspace"), project.slug);
if (path instanceof NextResponse) return path;
const cmd = `cd ${shq(path)} && ls -lA --time-style=long-iso 2>&1 | head -200`;
const r = await runFsCmd(principal, project, cmd);
@@ -5003,7 +5032,7 @@ async function toolFsDelete(principal: Principal, params: Record<string, any>) {
if (guard) return guard;
const project = await resolveProjectOr404(principal, params);
if (project instanceof NextResponse) return project;
const path = normalizeFsPath(String(params.path ?? ""));
const path = normalizeFsPath(String(params.path ?? ""), project.slug);
if (path instanceof NextResponse) return path;
const recursive = Boolean(params.recursive);
// Belt-and-suspenders: never let `rm -rf /workspace` itself slip through.
@@ -5036,7 +5065,7 @@ async function toolFsGlob(principal: Principal, params: Record<string, any>) {
{ status: 400 },
);
}
const cwd = normalizeFsPath(String(params.cwd ?? "/workspace"));
const cwd = normalizeFsPath(String(params.cwd ?? "/workspace"), project.slug);
if (cwd instanceof NextResponse) return cwd;
// ripgrep --files --glob is faster + smarter than `find` and respects .gitignore.
const cmd = `cd ${shq(cwd)} && rg --files --glob ${shq(pattern)} | head -500`;
@@ -5062,7 +5091,7 @@ async function toolFsGrep(principal: Principal, params: Record<string, any>) {
{ status: 400 },
);
}
const cwd = normalizeFsPath(String(params.cwd ?? "/workspace"));
const cwd = normalizeFsPath(String(params.cwd ?? "/workspace"), project.slug);
if (cwd instanceof NextResponse) return cwd;
const glob =
typeof params.glob === "string" && params.glob.trim()
@@ -6005,7 +6034,7 @@ async function toolFsTree(principal: Principal, params: Record<string, any>) {
if (guard) return guard;
const project = await resolveProjectOr404(principal, params);
if (project instanceof NextResponse) return project;
const path = normalizeFsPath(String(params.path ?? "/workspace"));
const path = normalizeFsPath(String(params.path ?? "/workspace"), project.slug);
if (path instanceof NextResponse) return path;
// Use find to generate a tree structure, ignoring node_modules and .git