chore(telemetry): flatten the project slug layer and remove cd path instructions from system prompt

This commit is contained in:
2026-06-09 13:28:57 -07:00
parent b5b18ccd32
commit 6ec312f716
3 changed files with 45 additions and 34 deletions

View File

@@ -219,9 +219,9 @@ ${activeProject.kickoff ? `- Created via: ${activeProject.kickoff.mode} (${JSON.
${decisionsBlock}${tasksBlock}${ideasBlock}${designKitBlock ? `\n${designKitBlock}\n` : ""}${codebaseBlock} ${decisionsBlock}${tasksBlock}${ideasBlock}${designKitBlock ? `\n${designKitBlock}\n` : ""}${codebaseBlock}
When you call tools that take a \`projectId\`, USE this id (\`${activeProject.id}\`) without asking. When the user says "this project" / "the app" / "deploy it" — they mean THIS project. Switch to a different project only if the user names one explicitly. When you call tools that take a \`projectId\`, USE this id (\`${activeProject.id}\`) without asking. When the user says "this project" / "the app" / "deploy it" — they mean THIS project. Switch to a different project only if the user names one explicitly.
**Project repo is auto-cloned at \`/workspace/${activeProject.slug ?? "<slug>"}/\` inside the dev container.** That path is the project's Gitea repo. ALL code, docs, configs, and other artifacts you intend the user to see in the Product tab MUST live under that path. Anything you write outside it (e.g. \`/workspace/scratch\`, \`/workspace/some-cloned-other-repo\`) is treated as scratch and is invisible in the UI. **Project repo is auto-cloned at \`/workspace/\` inside the dev container.** That path is the project's Gitea repo. ALL code, docs, configs, and other artifacts you intend the user to see in the Product tab MUST live under that path. Anything you write outside it (e.g. \`/workspace/scratch\`, \`/workspace/some-cloned-other-repo\`) is treated as scratch and is invisible in the UI.
After every assistant turn, the harness automatically runs \`git add -A && git commit && git push\` against \`/workspace/${activeProject.slug ?? "<slug>"}/\`. You do NOT need to commit manually unless the user asks for a specific commit message or you want to checkpoint mid-turn. Don't apologize for "forgetting to commit" — the harness handles it.\n` After every assistant turn, the harness automatically runs \`git add -A && git commit && git push\` against \`/workspace/\`. You do NOT need to commit manually unless the user asks for a specific commit message or you want to checkpoint mid-turn. Don't apologize for "forgetting to commit" — the harness handles it.\n`
: ""; : "";
return `You are Vibn AI — the technical co-founder of every Vibn user. You turn ideas into shipped software. Treat their projects like they're your own. return `You are Vibn AI — the technical co-founder of every Vibn user. You turn ideas into shipped software. Treat their projects like they're your own.
@@ -303,7 +303,7 @@ Each project has a persistent \`vibn-dev\` container. Edit files via \`fs_*\` an
**Dev servers (preview URL via \`*.preview.vibnai.com\` wildcard):** **Dev servers (preview URL via \`*.preview.vibnai.com\` wildcard):**
- \`dev_server_start { projectId, command, port: 3000 }\` is a **one-shot** call. It kills old processes on the port, checks the port is free, sets HOST=0.0.0.0 + PORT, launches your command, and returns a clickable \`previewUrl\`. Do NOT pre-flight with \`devcontainer_status\`, \`fs_list\`, \`dev_server_logs\`, or manual \`shell_exec\` kills — the function handles all of that. Just call it. The error tells you what to fix: \`PORT_BUSY\` → pick 30013009; \`npm: command not found\` → project needs \`npm install\` first. - \`dev_server_start { projectId, command, port: 3000 }\` is a **one-shot** call. It kills old processes on the port, checks the port is free, sets HOST=0.0.0.0 + PORT, launches your command, and returns a clickable \`previewUrl\`. Do NOT pre-flight with \`devcontainer_status\`, \`fs_list\`, \`dev_server_logs\`, or manual \`shell_exec\` kills — the function handles all of that. Just call it. The error tells you what to fix: \`PORT_BUSY\` → pick 30013009; \`npm: command not found\` → project needs \`npm install\` first.
- **Port:** The primary frontend service MUST ALWAYS be bound to port \`3000\`. Do not use any other port for the user-facing UI. If you are spinning up secondary services (like an API or Storybook) alongside it, you may bind them to ports \`30013009\`, but port \`3000\` is reserved exclusively for the primary visual preview. - **Port:** The primary frontend service MUST ALWAYS be bound to port \`3000\`. Do not use any other port for the user-facing UI. If you are spinning up secondary services (like an API or Storybook) alongside it, you may bind them to ports \`30013009\`, but port \`3000\` is reserved exclusively for the primary visual preview.
- **Directory:** The command runs from the root \`/workspace\` directory, but your project code is inside \`/workspace/\${activeProject.slug ?? "<slug>"}/\`. You MUST \`cd\` into your project folder first! Example: \`command: "cd \${activeProject.slug ?? "<slug>"} && npm run dev"\`. - **Directory:** The command runs from the root \`/workspace\` directory. Cwd is automatically set to \`/workspace\`. You do NOT need to run \`cd\` commands. Example: \`command: \"npm run dev\"\`.
- \`dev_server_stop\` / \`dev_server_list\` / \`dev_server_logs\` — use only AFTER a failed start, and only to diagnose the error the function returned. Never on success. - \`dev_server_stop\` / \`dev_server_list\` / \`dev_server_logs\` — use only AFTER a failed start, and only to diagnose the error the function returned. Never on success.
**Verify the page actually renders:** **Verify the page actually renders:**

View File

@@ -4576,7 +4576,7 @@ function normalizeFsPath(
p = p.substring("getacquired-2-0/".length); p = p.substring("getacquired-2-0/".length);
} }
const projectRoot = projectSlug ? `${FS_ROOT}/${projectSlug}` : FS_ROOT; const projectRoot = FS_ROOT;
let abs: string; let abs: string;
if (p.startsWith("/")) { if (p.startsWith("/")) {
abs = p; abs = p;

View File

@@ -34,21 +34,21 @@
* widen it. * widen it.
*/ */
import { execInDevContainer } from '@/lib/dev-container'; import { execInDevContainer } from "@/lib/dev-container";
const GITEA_API_URL = process.env.GITEA_API_URL ?? ''; const GITEA_API_URL = process.env.GITEA_API_URL ?? "";
// Falls back to GITEA_ADMIN_USER because production historically only set the // Falls back to GITEA_ADMIN_USER because production historically only set the
// admin var; missing GITEA_USERNAME used to silently disable auto-clone. // admin var; missing GITEA_USERNAME used to silently disable auto-clone.
const GITEA_USERNAME = const GITEA_USERNAME =
process.env.GITEA_USERNAME || process.env.GITEA_ADMIN_USER || ''; process.env.GITEA_USERNAME || process.env.GITEA_ADMIN_USER || "";
const GITEA_API_TOKEN = process.env.GITEA_API_TOKEN ?? ''; const GITEA_API_TOKEN = process.env.GITEA_API_TOKEN ?? "";
const AI_GIT_AUTHOR_NAME = 'Vibn AI'; const AI_GIT_AUTHOR_NAME = "Vibn AI";
const AI_GIT_AUTHOR_EMAIL = 'ai@vibnai.com'; const AI_GIT_AUTHOR_EMAIL = "ai@vibnai.com";
/** Where each project's repo lives inside the dev container. */ /** Where each project's repo lives inside the dev container. */
export function projectRepoPath(projectSlug: string): string { export function projectRepoPath(projectSlug: string): string {
return `/workspace/${projectSlug}`; return `/workspace`;
} }
function isGiteaConfigured(): boolean { function isGiteaConfigured(): boolean {
@@ -100,10 +100,14 @@ export async function ensureProjectRepoCloned(
opts: EnsureRepoClonedOpts, opts: EnsureRepoClonedOpts,
): Promise<EnsureRepoClonedResult> { ): Promise<EnsureRepoClonedResult> {
if (!isGiteaConfigured()) { if (!isGiteaConfigured()) {
return { cloned: false, alreadyPresent: false, reason: 'gitea_not_configured' }; return {
cloned: false,
alreadyPresent: false,
reason: "gitea_not_configured",
};
} }
if (!opts.giteaCloneUrl) { if (!opts.giteaCloneUrl) {
return { cloned: false, alreadyPresent: false, reason: 'no_clone_url' }; return { cloned: false, alreadyPresent: false, reason: "no_clone_url" };
} }
const repoDir = projectRepoPath(opts.projectSlug); const repoDir = projectRepoPath(opts.projectSlug);
@@ -121,18 +125,18 @@ export async function ensureProjectRepoCloned(
const probe = await execInDevContainer({ const probe = await execInDevContainer({
projectId: opts.projectId, projectId: opts.projectId,
command: command:
`if [ -d ${shellQ(repoDir + '/.git')} ]; then echo git; ` + `if [ -d ${shellQ(repoDir + "/.git")} ]; then echo git; ` +
`elif [ -d ${shellQ(repoDir)} ]; then echo dir; ` + `elif [ -d ${shellQ(repoDir)} ]; then echo dir; ` +
`else echo absent; fi`, `else echo absent; fi`,
timeoutMs: 5_000, timeoutMs: 5_000,
}); });
const probeState = probe.stdout.trim(); const probeState = probe.stdout.trim();
if (probeState === 'git') { if (probeState === "git") {
return { cloned: false, alreadyPresent: true }; return { cloned: false, alreadyPresent: true };
} }
if (probeState === 'dir') { if (probeState === "dir") {
// Init in place, hook up the remote, fetch, set tracking. We // Init in place, hook up the remote, fetch, set tracking. We
// don't try to merge — the directory's contents become whatever // don't try to merge — the directory's contents become whatever
// the next auto-commit-and-push picks up. If the remote has // the next auto-commit-and-push picks up. If the remote has
@@ -147,7 +151,7 @@ export async function ensureProjectRepoCloned(
// Best-effort fetch; if remote is empty this errors out and // Best-effort fetch; if remote is empty this errors out and
// we proceed anyway. The `|| true` keeps the chain going. // we proceed anyway. The `|| true` keeps the chain going.
`(git fetch origin main 2>/dev/null && git reset --soft origin/main 2>/dev/null) || true`, `(git fetch origin main 2>/dev/null && git reset --soft origin/main 2>/dev/null) || true`,
].join(' && '); ].join(" && ");
const result = await execInDevContainer({ const result = await execInDevContainer({
projectId: opts.projectId, projectId: opts.projectId,
@@ -182,7 +186,7 @@ export async function ensureProjectRepoCloned(
`git config user.email ${shellQ(AI_GIT_AUTHOR_EMAIL)}`, `git config user.email ${shellQ(AI_GIT_AUTHOR_EMAIL)}`,
`git remote set-url origin ${shellQ(authed)}`, `git remote set-url origin ${shellQ(authed)}`,
`cd /workspace && mv ${shellQ(tmpDir)} ${shellQ(repoDir)}`, `cd /workspace && mv ${shellQ(tmpDir)} ${shellQ(repoDir)}`,
].join(' && '); ].join(" && ");
const result = await execInDevContainer({ const result = await execInDevContainer({
projectId: opts.projectId, projectId: opts.projectId,
@@ -201,7 +205,11 @@ export async function ensureProjectRepoCloned(
// (warning: "remote HEAD refers to nonexistent ref"), clone // (warning: "remote HEAD refers to nonexistent ref"), clone
// didn't actually fail — just there's no history to check // didn't actually fail — just there's no history to check
// out. Make an empty dir + init so subsequent commits land. // out. Make an empty dir + init so subsequent commits land.
if (/empty|warning: You appear to have cloned an empty/i.test(result.stderr + result.stdout)) { if (
/empty|warning: You appear to have cloned an empty/i.test(
result.stderr + result.stdout,
)
) {
const initEmpty = [ const initEmpty = [
`mkdir -p ${shellQ(repoDir)}`, `mkdir -p ${shellQ(repoDir)}`,
`cd ${shellQ(repoDir)}`, `cd ${shellQ(repoDir)}`,
@@ -209,7 +217,7 @@ export async function ensureProjectRepoCloned(
`git config user.name ${shellQ(AI_GIT_AUTHOR_NAME)}`, `git config user.name ${shellQ(AI_GIT_AUTHOR_NAME)}`,
`git config user.email ${shellQ(AI_GIT_AUTHOR_EMAIL)}`, `git config user.email ${shellQ(AI_GIT_AUTHOR_EMAIL)}`,
`git remote add origin ${shellQ(authed)}`, `git remote add origin ${shellQ(authed)}`,
].join(' && '); ].join(" && ");
const initR = await execInDevContainer({ const initR = await execInDevContainer({
projectId: opts.projectId, projectId: opts.projectId,
command: initEmpty, command: initEmpty,
@@ -260,21 +268,21 @@ export async function commitAndPushIfDirty(
opts: CommitAndPushOpts, opts: CommitAndPushOpts,
): Promise<CommitAndPushResult> { ): Promise<CommitAndPushResult> {
const repoDir = projectRepoPath(opts.projectSlug); const repoDir = projectRepoPath(opts.projectSlug);
const message = (opts.message ?? '').trim() || 'AI checkpoint'; const message = (opts.message ?? "").trim() || "AI checkpoint";
// Sanitize: keep messages single-line and bounded so we can't be // Sanitize: keep messages single-line and bounded so we can't be
// tricked into shell-escape or commit-message injection. // tricked into shell-escape or commit-message injection.
const safeMessage = message.replace(/[\r\n]+/g, ' ').slice(0, 200); const safeMessage = message.replace(/[\r\n]+/g, " ").slice(0, 200);
// Bail fast if there's no repo to commit against. Don't treat as // Bail fast if there's no repo to commit against. Don't treat as
// an error — projects without a clone (e.g. cloning failed earlier) // an error — projects without a clone (e.g. cloning failed earlier)
// should still be able to chat. // should still be able to chat.
const probe = await execInDevContainer({ const probe = await execInDevContainer({
projectId: opts.projectId, projectId: opts.projectId,
command: `test -d ${shellQ(repoDir + '/.git')} && echo present || echo absent`, command: `test -d ${shellQ(repoDir + "/.git")} && echo present || echo absent`,
timeoutMs: 5_000, timeoutMs: 5_000,
}).catch(() => null); }).catch(() => null);
if (!probe || probe.stdout.trim() !== 'present') { if (!probe || probe.stdout.trim() !== "present") {
return { committed: false, pushed: false, reason: 'no_repo' }; return { committed: false, pushed: false, reason: "no_repo" };
} }
const cmd = [ const cmd = [
@@ -287,21 +295,24 @@ export async function commitAndPushIfDirty(
`SHA=$(git rev-parse --short HEAD)`, `SHA=$(git rev-parse --short HEAD)`,
`git push -u origin HEAD 2>&1 | tail -n 5`, `git push -u origin HEAD 2>&1 | tail -n 5`,
`echo COMMITTED $SHA`, `echo COMMITTED $SHA`,
].join(' && '); ].join(" && ");
const result = await execInDevContainer({ const result = await execInDevContainer({
projectId: opts.projectId, projectId: opts.projectId,
command: cmd, command: cmd,
timeoutMs: 30_000, timeoutMs: 30_000,
}).catch((err: unknown) => ({ }).catch(
stdout: '', (err: unknown) =>
stderr: err instanceof Error ? err.message : String(err), ({
exitCode: -1, stdout: "",
} satisfies { stdout: string; stderr: string; exitCode: number })); stderr: err instanceof Error ? err.message : String(err),
exitCode: -1,
}) satisfies { stdout: string; stderr: string; exitCode: number },
);
const out = result.stdout || ''; const out = result.stdout || "";
if (out.includes('CLEAN')) { if (out.includes("CLEAN")) {
return { committed: false, pushed: false, reason: 'clean' }; return { committed: false, pushed: false, reason: "clean" };
} }
const m = /COMMITTED\s+([0-9a-f]+)/.exec(out); const m = /COMMITTED\s+([0-9a-f]+)/.exec(out);
if (m) { if (m) {