Compare commits

...

7 Commits

Author SHA1 Message Date
419af40ca2 feat(agent): POST timeline events to vibn-frontend ingest API
- vibn-events-ingest.ts + emit() dual-write with session PATCH
- .env.example: VIBN_API_URL, AGENT_RUNNER_SECRET

Made-with: Cursor
2026-04-01 11:48:57 -07:00
1ff020cf53 fix(prompt): require all 12 PRD sections, no truncation, finalize only when complete
Made-with: Cursor
2026-03-17 17:10:07 -07:00
1cb173a822 fix(prompt): save phase and continue immediately — no confirmation wait
- Rule 3: replace 'get explicit confirmation' with 'summarize and keep moving'
- Checkpoint rule: append marker immediately, then continue to next phase in same message
- Brain dump edge case: save all phases, chain PHASE_COMPLETE markers, no pausing

Made-with: Cursor
2026-03-17 17:00:25 -07:00
9b1f25fa4a rename: replace Atlas with Vibn in agent system prompt and greeting
Made-with: Cursor
2026-03-17 16:25:47 -07:00
772f5357a8 Fix Atlas init: add user turn so Gemini doesn't reject empty conversation
When is_init=true, no user message was being added to history before
calling the LLM. Gemini requires at least one user turn — without it
the API returned "contents are required" and Atlas never sent its
opening greeting. Now adds the init message marked internally so it's
sent to the LLM but filtered out of returned/stored history.

Made-with: Cursor
2026-03-17 15:56:50 -07:00
8ae640c911 feat: update orchestrator prompt and knowledge context injection
- Rewrite system prompt to support dual-mode: COO for user projects
  when knowledge_context provides project data, or platform orchestrator
  when called without context
- Remove the "Project Memory" wrapper prefix so knowledge_context is
  injected cleanly (the COO persona header in context is self-contained)
- Clarify tools, style, security rules

Made-with: Cursor
2026-03-09 22:32:05 -07:00
ba4b94790c feat(mirror): support GitHub PAT for private repo mirroring
Accept optional github_token in POST /api/mirror and inject it into
the git clone URL so private repos can be cloned without interactive auth.

Made-with: Cursor
2026-03-09 18:05:09 -07:00
12 changed files with 288 additions and 54 deletions

View File

@@ -45,5 +45,12 @@ WORKSPACE_BASE=/workspaces
# Internal URL of this service (used by spawn_agent to self-call) # Internal URL of this service (used by spawn_agent to self-call)
AGENT_RUNNER_URL=http://localhost:3333 AGENT_RUNNER_URL=http://localhost:3333
# Base URL of the vibn-frontend Next app (runner PATCHes sessions + POSTs timeline events)
# Production: https://vibnai.com (must be reachable from this container)
VIBN_API_URL=http://localhost:3000
# Same value as AGENT_RUNNER_SECRET on the Next app (ingest + session PATCH)
AGENT_RUNNER_SECRET=
# Optional: shared secret for validating Gitea webhook POSTs # Optional: shared secret for validating Gitea webhook POSTs
WEBHOOK_SECRET= WEBHOOK_SECRET=

View File

@@ -23,6 +23,13 @@ export interface SessionRunOptions {
projectId: string; projectId: string;
vibnApiUrl: string; vibnApiUrl: string;
appPath: string; appPath: string;
repoRoot?: string;
isStopped: () => boolean; isStopped: () => boolean;
autoApprove?: boolean;
giteaRepo?: string;
coolifyAppUuid?: string;
coolifyApiUrl?: string;
coolifyApiToken?: string;
theiaWorkspaceSubdir?: string;
} }
export declare function runSessionAgent(config: AgentConfig, task: string, ctx: ToolContext, opts: SessionRunOptions): Promise<void>; export declare function runSessionAgent(config: AgentConfig, task: string, ctx: ToolContext, opts: SessionRunOptions): Promise<void>;

View File

@@ -14,9 +14,12 @@
*/ */
Object.defineProperty(exports, "__esModule", { value: true }); Object.defineProperty(exports, "__esModule", { value: true });
exports.runSessionAgent = runSessionAgent; exports.runSessionAgent = runSessionAgent;
const child_process_1 = require("child_process");
const llm_1 = require("./llm"); const llm_1 = require("./llm");
const tools_1 = require("./tools"); const tools_1 = require("./tools");
const loader_1 = require("./prompts/loader"); const loader_1 = require("./prompts/loader");
const theia_exec_1 = require("./theia-exec");
const vibn_events_ingest_1 = require("./vibn-events-ingest");
const MAX_TURNS = 60; const MAX_TURNS = 60;
// ── VIBN DB bridge ──────────────────────────────────────────────────────────── // ── VIBN DB bridge ────────────────────────────────────────────────────────────
async function patchSession(opts, payload) { async function patchSession(opts, payload) {
@@ -53,13 +56,90 @@ function extractChangedFile(toolName, args, workspaceRoot, appPath) {
const fileStatus = toolName === 'write_file' ? 'added' : 'modified'; const fileStatus = toolName === 'write_file' ? 'added' : 'modified';
return { path: displayPath, status: fileStatus }; return { path: displayPath, status: fileStatus };
} }
// ── Auto-commit helper ────────────────────────────────────────────────────────
async function autoCommitAndDeploy(opts, task, emit) {
const repoRoot = opts.repoRoot;
if (!repoRoot || !opts.giteaRepo) {
await emit({ ts: now(), type: 'info', text: 'Auto-approve skipped — no repo root available.' });
return;
}
const gitOpts = { cwd: repoRoot, stdio: 'pipe' };
const giteaApiUrl = process.env.GITEA_API_URL || '';
const giteaUsername = process.env.GITEA_USERNAME || 'agent';
const giteaToken = process.env.GITEA_API_TOKEN || '';
try {
// Sync files into Theia via the sync-server so "Open in Theia" shows latest code
if (opts.giteaRepo && await (0, theia_exec_1.isTheiaSyncAvailable)()) {
await emit({ ts: now(), type: 'info', text: `Syncing to Theia…` });
const syncResult = await (0, theia_exec_1.syncRepoToTheia)(opts.giteaRepo);
if (syncResult.ok) {
await emit({ ts: now(), type: 'info', text: `✓ Theia synced (${syncResult.action}) — open theia.vibnai.com to inspect.` });
}
else {
console.warn('[session-runner] Theia sync failed:', syncResult.error);
}
}
try {
(0, child_process_1.execSync)('git config user.email "agent@vibnai.com"', gitOpts);
(0, child_process_1.execSync)('git config user.name "VIBN Agent"', gitOpts);
}
catch { /* already set */ }
(0, child_process_1.execSync)('git add -A', gitOpts);
const status = (0, child_process_1.execSync)('git status --porcelain', gitOpts).toString().trim();
if (!status) {
await emit({ ts: now(), type: 'info', text: '✓ No file changes to commit.' });
await patchSession(opts, { status: 'approved' });
return;
}
const commitMsg = `agent: ${task.slice(0, 72)}`;
(0, child_process_1.execSync)(`git commit -m ${JSON.stringify(commitMsg)}`, gitOpts);
await emit({ ts: now(), type: 'info', text: `✓ Committed: "${commitMsg}"` });
const authedUrl = `${giteaApiUrl}/${opts.giteaRepo}.git`
.replace('https://', `https://${giteaUsername}:${giteaToken}@`);
(0, child_process_1.execSync)(`git push "${authedUrl}" HEAD:main`, gitOpts);
await emit({ ts: now(), type: 'info', text: '✓ Pushed to Gitea.' });
// Optional Coolify deploy
let deployed = false;
if (opts.coolifyApiUrl && opts.coolifyApiToken && opts.coolifyAppUuid) {
try {
const deployRes = await fetch(`${opts.coolifyApiUrl}/api/v1/applications/${opts.coolifyAppUuid}/start`, { method: 'POST', headers: { Authorization: `Bearer ${opts.coolifyApiToken}` } });
deployed = deployRes.ok;
if (deployed)
await emit({ ts: now(), type: 'info', text: '✓ Deployment triggered.' });
}
catch { /* best-effort */ }
}
await patchSession(opts, {
status: 'approved',
outputLine: {
ts: now(), type: 'done',
text: `✓ Auto-committed & ${deployed ? 'deployed' : 'pushed'}. No approval needed.`,
},
});
}
catch (err) {
const msg = err instanceof Error ? err.message : String(err);
await emit({ ts: now(), type: 'error', text: `Auto-commit failed: ${msg}` });
// Fall back to done so user can manually approve
await patchSession(opts, { status: 'done' });
}
}
// ── Main streaming execution loop ───────────────────────────────────────────── // ── Main streaming execution loop ─────────────────────────────────────────────
async function runSessionAgent(config, task, ctx, opts) { async function runSessionAgent(config, task, ctx, opts) {
const llm = (0, llm_1.createLLM)(config.model, { temperature: 0.2 }); const llm = (0, llm_1.createLLM)(config.model, { temperature: 0.2 });
const oaiTools = (0, llm_1.toOAITools)(config.tools); const oaiTools = (0, llm_1.toOAITools)(config.tools);
const emit = async (line) => { const emit = async (line) => {
console.log(`[session ${opts.sessionId}] ${line.type}: ${line.text}`); console.log(`[session ${opts.sessionId}] ${line.type}: ${line.text}`);
await patchSession(opts, { outputLine: line }); await Promise.all([
patchSession(opts, { outputLine: line }),
(0, vibn_events_ingest_1.ingestSessionEvents)(opts.vibnApiUrl, opts.projectId, opts.sessionId, [
{
type: `output.${line.type}`,
payload: { text: line.text },
ts: line.ts,
},
]),
]);
}; };
await emit({ ts: now(), type: 'info', text: `Agent starting (${llm.modelId}) — working in ${opts.appPath}` }); await emit({ ts: now(), type: 'info', text: `Agent starting (${llm.modelId}) — working in ${opts.appPath}` });
// Scope the system prompt to the specific app within the monorepo // Scope the system prompt to the specific app within the monorepo
@@ -70,7 +150,7 @@ async function runSessionAgent(config, task, ctx, opts) {
You are working inside the monorepo directory: ${opts.appPath} You are working inside the monorepo directory: ${opts.appPath}
All file paths you use should be relative to this directory unless otherwise specified. All file paths you use should be relative to this directory unless otherwise specified.
When running commands, always cd into ${opts.appPath} first unless already there. When running commands, always cd into ${opts.appPath} first unless already there.
When you are done, do NOT commit directly — leave the changes uncommitted so the user can review and approve them. Do NOT run git commit or git push — the platform handles committing after you finish.
`; `;
const history = [ const history = [
{ role: 'user', content: task } { role: 'user', content: task }
@@ -127,7 +207,23 @@ When you are done, do NOT commit directly — leave the changes uncommitted so t
await emit({ ts: now(), type: 'step', text: stepLabel }); await emit({ ts: now(), type: 'step', text: stepLabel });
let result; let result;
try { try {
result = await (0, tools_1.executeTool)(fnName, fnArgs, ctx); // Route execute_command through Theia when available so npm/node
// commands run inside Theia's persistent dev environment
if (fnName === 'execute_command' && (0, theia_exec_1.isTheiaAvailable)()) {
const command = String(fnArgs.command ?? '');
const subCwd = fnArgs.working_directory
? `${opts.theiaWorkspaceSubdir ?? ''}/${fnArgs.working_directory}`.replace(/\/+/g, '/')
: opts.theiaWorkspaceSubdir ?? undefined;
result = await (0, theia_exec_1.theiaExec)(command, subCwd ? `${process.env.THEIA_WORKSPACE ?? '/home/node/workspace'}/${subCwd}` : undefined);
if (result?.error && result?.exitCode !== 0) {
// Fallback to local execution if Theia exec fails
console.warn('[session-runner] Theia exec failed, falling back to local:', result.error);
result = await (0, tools_1.executeTool)(fnName, fnArgs, ctx);
}
}
else {
result = await (0, tools_1.executeTool)(fnName, fnArgs, ctx);
}
} }
catch (err) { catch (err) {
result = { error: err instanceof Error ? err.message : String(err) }; result = { error: err instanceof Error ? err.message : String(err) };
@@ -168,10 +264,15 @@ When you are done, do NOT commit directly — leave the changes uncommitted so t
finalText = `Hit the ${MAX_TURNS}-turn limit. Stopping.`; finalText = `Hit the ${MAX_TURNS}-turn limit. Stopping.`;
} }
await emit({ ts: now(), type: 'done', text: finalText }); await emit({ ts: now(), type: 'done', text: finalText });
await patchSession(opts, { if (opts.autoApprove) {
status: 'done', await autoCommitAndDeploy(opts, task, emit);
outputLine: { ts: now(), type: 'done', text: '✓ Complete — review changes and approve to commit.' } }
}); else {
await patchSession(opts, {
status: 'done',
outputLine: { ts: now(), type: 'done', text: '✓ Complete — review changes and approve to commit.' },
});
}
} }
// ── Step label helpers ──────────────────────────────────────────────────────── // ── Step label helpers ────────────────────────────────────────────────────────
function buildStepLabel(tool, args) { function buildStepLabel(tool, args) {

6
dist/vibn-events-ingest.d.ts vendored Normal file
View File

@@ -0,0 +1,6 @@
export interface IngestEventInput {
type: string;
payload?: Record<string, unknown>;
ts?: string;
}
export declare function ingestSessionEvents(vibnApiUrl: string, projectId: string, sessionId: string, events: IngestEventInput[]): Promise<void>;

39
dist/vibn-events-ingest.js vendored Normal file
View File

@@ -0,0 +1,39 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ingestSessionEvents = ingestSessionEvents;
/**
* Push structured timeline events to vibn-frontend (Postgres via ingest API).
* Complements PATCH output lines — enables SSE replay without polling every line.
*/
const crypto_1 = require("crypto");
async function ingestSessionEvents(vibnApiUrl, projectId, sessionId, events) {
if (events.length === 0)
return;
const secret = process.env.AGENT_RUNNER_SECRET ?? '';
const base = vibnApiUrl.replace(/\/$/, '');
const url = `${base}/api/projects/${projectId}/agent/sessions/${sessionId}/events`;
try {
const res = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-agent-runner-secret': secret,
},
body: JSON.stringify({
events: events.map((e) => ({
clientEventId: (0, crypto_1.randomUUID)(),
ts: e.ts ?? new Date().toISOString(),
type: e.type,
payload: e.payload ?? {},
})),
}),
});
if (!res.ok) {
const t = await res.text();
console.warn('[ingest-events]', res.status, t.slice(0, 240));
}
}
catch (err) {
console.warn('[ingest-events]', err instanceof Error ? err.message : err);
}
}

View File

@@ -18,6 +18,7 @@ import { AgentConfig } from './agents';
import { executeTool, ToolContext } from './tools'; import { executeTool, ToolContext } from './tools';
import { resolvePrompt } from './prompts/loader'; import { resolvePrompt } from './prompts/loader';
import { isTheiaAvailable, theiaExec, syncRepoToTheia, isTheiaSyncAvailable } from './theia-exec'; import { isTheiaAvailable, theiaExec, syncRepoToTheia, isTheiaSyncAvailable } from './theia-exec';
import { ingestSessionEvents } from './vibn-events-ingest';
const MAX_TURNS = 60; const MAX_TURNS = 60;
@@ -191,7 +192,16 @@ export async function runSessionAgent(
const emit = async (line: OutputLine) => { const emit = async (line: OutputLine) => {
console.log(`[session ${opts.sessionId}] ${line.type}: ${line.text}`); console.log(`[session ${opts.sessionId}] ${line.type}: ${line.text}`);
await patchSession(opts, { outputLine: line }); await Promise.all([
patchSession(opts, { outputLine: line }),
ingestSessionEvents(opts.vibnApiUrl, opts.projectId, opts.sessionId, [
{
type: `output.${line.type}`,
payload: { text: line.text },
ts: line.ts,
},
]),
]);
}; };
await emit({ ts: now(), type: 'info', text: `Agent starting (${llm.modelId}) — working in ${opts.appPath}` }); await emit({ ts: now(), type: 'info', text: `Agent starting (${llm.modelId}) — working in ${opts.appPath}` });

View File

@@ -88,14 +88,23 @@ export async function atlasChat(
const oaiTools = toOAITools(ATLAS_TOOLS); const oaiTools = toOAITools(ATLAS_TOOLS);
const systemPrompt = resolvePrompt('atlas'); const systemPrompt = resolvePrompt('atlas');
// For init triggers, don't add the synthetic prompt as a user turn // Always push the user message so Gemini gets a valid conversation (requires at least one user turn).
if (!opts?.isInit) { // For init triggers, we mark it so we can strip it from the returned history — it's an internal
session.history.push({ role: 'user', content: userMessage }); // prompt, not a real user message, and shouldn't appear in the conversation UI or DB.
} const INIT_MARKER = '__atlas_init_marker__';
session.history.push({
role: 'user',
content: opts?.isInit ? INIT_MARKER + userMessage : userMessage
});
const buildMessages = (): LLMMessage[] => [ const buildMessages = (): LLMMessage[] => [
{ role: 'system', content: systemPrompt }, { role: 'system', content: systemPrompt },
...session.history.slice(-60) ...session.history.slice(-60).map(m =>
// Strip the init marker before sending to the LLM
m.role === 'user' && typeof m.content === 'string' && m.content.startsWith(INIT_MARKER)
? { ...m, content: m.content.slice(INIT_MARKER.length) }
: m
)
]; ];
let turn = 0; let turn = 0;
@@ -156,6 +165,8 @@ export async function atlasChat(
reply: finalReply, reply: finalReply,
sessionId, sessionId,
history: session.history history: session.history
// Drop the internal init user turn — it's not a real user message
.filter(m => !(m.role === 'user' && typeof m.content === 'string' && m.content.startsWith(INIT_MARKER)))
.filter(m => m.role !== 'assistant' || m.content || m.tool_calls?.length) .filter(m => m.role !== 'assistant' || m.content || m.tool_calls?.length)
.slice(-60), .slice(-60),
prdContent, prdContent,

View File

@@ -99,11 +99,9 @@ export async function orchestratorChat(
let finalReasoning: string | null = null; let finalReasoning: string | null = null;
const toolCallNames: string[] = []; const toolCallNames: string[] = [];
// Resolve system prompt from template — {{knowledge}} injects project memory // Resolve system prompt from template — {{knowledge}} injects project/COO context
const systemContent = resolvePrompt('orchestrator', { const systemContent = resolvePrompt('orchestrator', {
knowledge: opts?.knowledgeContext knowledge: opts?.knowledgeContext ?? ''
? `## Project Memory (known facts)\n${opts.knowledgeContext}`
: ''
}); });
// Build messages with system prompt prepended; keep last 40 for cost control // Build messages with system prompt prepended; keep last 40 for cost control

View File

@@ -1,7 +1,7 @@
import { registerPrompt } from './loader'; import { registerPrompt } from './loader';
registerPrompt('atlas', ` registerPrompt('atlas', `
You are Atlas, a senior product strategist and requirements architect. You guide product managers, founders, and non-technical stakeholders through the process of defining, scoping, and documenting a software product — from a rough idea to a comprehensive, implementation-ready Product Requirements Document (PRD). You are Vibn, a senior product strategist and requirements architect. You guide product managers, founders, and non-technical stakeholders through the process of defining, scoping, and documenting a software product — from a rough idea to a comprehensive, implementation-ready Product Requirements Document (PRD).
You think like a principal PM at a top-tier product company: structured, pragmatic, user-obsessed, and cost-aware. You ask the right questions at the right time, challenge weak assumptions, and help people build the right thing — not just the thing they first described. You think like a principal PM at a top-tier product company: structured, pragmatic, user-obsessed, and cost-aware. You ask the right questions at the right time, challenge weak assumptions, and help people build the right thing — not just the thing they first described.
@@ -11,7 +11,7 @@ You never expose technical implementation details (databases, frameworks, hostin
1. **Lead with curiosity, not assumptions.** Never generate a full PRD from a single sentence. Conduct a structured discovery conversation first. 1. **Lead with curiosity, not assumptions.** Never generate a full PRD from a single sentence. Conduct a structured discovery conversation first.
2. **One phase at a time.** Move through the discovery phases sequentially. Don't skip ahead unless the user provides enough detail to justify it. 2. **One phase at a time.** Move through the discovery phases sequentially. Don't skip ahead unless the user provides enough detail to justify it.
3. **Summarize before advancing.** At the end of each phase, reflect back what you've learned and get explicit confirmation before moving on. 3. **Summarize and keep moving.** At the end of each phase, briefly reflect back what you captured in 23 sentences, then immediately save the phase and continue to the next one. Do NOT ask "Does that sound right?" or wait for the user to confirm before advancing. If you're uncertain about something specific, note it as a quick question while still moving forward.
4. **Challenge gently.** If the user's scope is too broad, their target audience is vague, or their feature list is a wishlist, push back constructively. Say things like: "That's a great long-term vision. For a strong v1, what's the one workflow that absolutely has to work on day one?" 4. **Challenge gently.** If the user's scope is too broad, their target audience is vague, or their feature list is a wishlist, push back constructively. Say things like: "That's a great long-term vision. For a strong v1, what's the one workflow that absolutely has to work on day one?"
5. **Stay in product language.** Talk about users, journeys, features, screens, roles, permissions, integrations, and business rules — not tables, endpoints, or deployment pipelines. 5. **Stay in product language.** Talk about users, journeys, features, screens, roles, permissions, integrations, and business rules — not tables, endpoints, or deployment pipelines.
6. **Respect what you don't know.** If the user's domain is unfamiliar, ask clarifying questions rather than guessing. Incorrect assumptions in a PRD are expensive. 6. **Respect what you don't know.** If the user's domain is unfamiliar, ask clarifying questions rather than guessing. Incorrect assumptions in a PRD are expensive.
@@ -122,7 +122,7 @@ Once all phases are complete (or the user indicates they have enough), generate
## 13. Appendix ## 13. Appendix
\`\`\` \`\`\`
The PRD should be specific enough that a technical team could implement it without further clarification on product intent. When you've finished writing the PRD, call the finalize_prd tool with the complete document. The PRD must include ALL sections 112 (skip 13. Appendix only if there is nothing to add). Do NOT truncate, summarise, or leave any section empty — write substantive content for every section based on what was discussed. If a section has limited information, write what you know and flag the gaps explicitly. Only call the finalize_prd tool once the entire document is written — do not call it mid-document.
## Conversation Style ## Conversation Style
@@ -149,9 +149,10 @@ Phase IDs and their key data fields:
- phase_id "risks_questions" → fields: risks (array), openQuestions (array), assumptions (array) - phase_id "risks_questions" → fields: risks (array), openQuestions (array), assumptions (array)
Rules: Rules:
- Only append the marker ONCE per phase, after explicit user confirmation of the summary. - Append the marker immediately after summarising the phase — do NOT wait for user confirmation first.
- After appending the marker, immediately continue to the next phase question in the same message. Do not pause and wait for the user to respond before asking the next phase's questions.
- Never guess — only include fields the user actually provided. Use null for unknown fields. - Never guess — only include fields the user actually provided. Use null for unknown fields.
- The marker will be hidden from the user and converted into a save button. Do not mention it. - The marker will be hidden from the user and converted into a save indicator. Do not mention it.
- Example: [[PHASE_COMPLETE:{"phase":"big_picture","title":"The Big Picture","summary":"Sportsy is a fantasy hockey management game inspired by OSM, targeting casual hockey fans aged 1835.","data":{"productName":"Sportsy","problemStatement":"No compelling fantasy hockey management game exists for casual fans","targetUser":"Casual hockey fans 1835","successMetric":"10k active users in 6 months","competitors":"OSM","deadline":null}}]] - Example: [[PHASE_COMPLETE:{"phase":"big_picture","title":"The Big Picture","summary":"Sportsy is a fantasy hockey management game inspired by OSM, targeting casual hockey fans aged 1835.","data":{"productName":"Sportsy","problemStatement":"No compelling fantasy hockey management game exists for casual fans","targetUser":"Casual hockey fans 1835","successMetric":"10k active users in 6 months","competitors":"OSM","deadline":null}}]]
## After the PRD Is Complete ## After the PRD Is Complete
@@ -199,7 +200,7 @@ Call it silently — don't announce you're searching. Just use the result to inf
## Handling Edge Cases ## Handling Edge Cases
- **User gives a massive brain dump:** Parse it, organize it into the phases, reflect it back structured, and identify gaps. - **User gives a massive brain dump:** Parse it, extract each phase's data, save all phases you have enough info for (one PHASE_COMPLETE marker per phase, each followed immediately by the next question or the next phase summary), then ask only about genuine gaps. Do not pause between phases for confirmation.
- **User wants to skip straight to the PRD:** "I can generate a PRD right now, but the best PRDs come from about 10 minutes of focused conversation. The questions I'll ask will save weeks of rework later. Want to do a quick run-through?" - **User wants to skip straight to the PRD:** "I can generate a PRD right now, but the best PRDs come from about 10 minutes of focused conversation. The questions I'll ask will save weeks of rework later. Want to do a quick run-through?"
- **User is vague:** Offer options — "Let me give you three common approaches and you tell me which feels closest…" - **User is vague:** Offer options — "Let me give you three common approaches and you tell me which feels closest…"
- **User changes direction mid-conversation:** Acknowledge the pivot and resurface downstream impacts. - **User changes direction mid-conversation:** Acknowledge the pivot and resurface downstream impacts.
@@ -209,7 +210,7 @@ Call it silently — don't announce you're searching. Just use the result to inf
When you receive an internal init trigger to begin a new conversation (no prior history), introduce yourself naturally: When you receive an internal init trigger to begin a new conversation (no prior history), introduce yourself naturally:
"Hey! I'm Atlas — I'm here to help you turn your product idea into a clear, detailed requirements document that's ready for implementation. "Hey! I'm Vibn — I'm here to help you turn your product idea into a clear, detailed requirements document that's ready for implementation.
Whether you've got a rough concept or a detailed spec that needs tightening, I'll walk you through the key decisions and make sure nothing important falls through the cracks. Whether you've got a rough concept or a detailed spec that needs tightening, I'll walk you through the key decisions and make sure nothing important falls through the cracks.

View File

@@ -1,29 +1,27 @@
import { registerPrompt } from './loader'; import { registerPrompt } from './loader';
registerPrompt('orchestrator', ` registerPrompt('orchestrator', `
You are the Master Orchestrator for Vibn — an AI-powered cloud development platform. You are an AI executive assistant with full tool access to act on behalf of a software founder.
You run continuously and have full awareness of the Vibn project. You can take autonomous action on behalf of the user. When project context is provided below, you are operating as the personal AI COO for that specific project — an executive partner to the founder. When no project context is provided, you operate as the Master Orchestrator for the Vibn platform itself.
## What Vibn is ## Platform context (always available)
Vibn lets developers build products using AI agents: - Vibn frontend: vibnai.com (Next.js)
- Frontend app (Next.js) at vibnai.com - Agent runner: agents.vibnai.com (this system)
- Backend API at api.vibnai.com - Self-hosted Git: git.vibnai.com (Gitea, user: mark)
- Agent runner (this system) at agents.vibnai.com - Deployments: Coolify at coolify.vibnai.com (server: 34.19.250.135, Montreal)
- Cloud IDE (Theia) at theia.vibnai.com - Cloud IDE: theia.vibnai.com
- Self-hosted Git at git.vibnai.com (user: mark)
- Deployments via Coolify at coolify.vibnai.com (server: 34.19.250.135, Montreal)
## Your tools ## Your tools
**Awareness** (understand current state first): **Awareness** (understand current state first):
- list_repos — all Git repositories - list_repos — all Gitea repositories
- list_all_issues — open/in-progress work - list_all_issues — open/in-progress work items
- list_all_apps — deployed apps and their status - list_all_apps — deployed apps and their status in Coolify
- get_app_status — health of a specific app - get_app_status — health of a specific app
- read_repo_file — read any file from any repo without cloning - read_repo_file — read any file from any repo without cloning
- list_skills — list available skills for a project repo - list_skills — list available skills for a project repo
- get_skill — read the full content of a specific skill - get_skill — read a skill's full content
**Action** (get things done): **Action** (get things done):
- spawn_agent — dispatch Coder, PM, or Marketing agent on a repo - spawn_agent — dispatch Coder, PM, or Marketing agent on a repo
@@ -31,31 +29,32 @@ Vibn lets developers build products using AI agents:
- deploy_app — trigger a Coolify deployment - deploy_app — trigger a Coolify deployment
- gitea_create_issue — track work (label agent:coder/pm/marketing to auto-trigger) - gitea_create_issue — track work (label agent:coder/pm/marketing to auto-trigger)
- gitea_list_issues / gitea_close_issue — issue lifecycle - gitea_list_issues / gitea_close_issue — issue lifecycle
- save_memory — persist important project facts across conversations - save_memory — persist important facts across conversations
## Specialist agents you can spawn ## Specialist agents you can spawn
- **Coder** — writes code, tests, commits, and pushes - **Coder** — writes code, tests, commits, pushes
- **PM** — docs, issues, sprint tracking - **PM** — docs, issues, sprint tracking
- **Marketing** — copy, release notes, blog posts - **Marketing** — copy, release notes, blog posts
## How you work ## How you work
1. Use awareness tools first if you need current state. 1. Use awareness tools first to understand the current state before acting.
2. Break the task into concrete steps. 2. Break tasks into concrete steps.
3. Before spawning an agent, call list_skills to check if relevant skills exist and pass them as context. 3. Before spawning an agent, call list_skills to check for relevant skills and pass them as context.
4. Spawn the right agent(s) with specific, detailed instructions. 4. Spawn the right agent(s) with specific, detailed instructions — never vague.
5. Track and report on results. 5. Track and report results.
6. If you notice something that needs attention (failed deploy, open bugs, stale issues), mention it proactively. 6. Proactively surface issues: failed deploys, open bugs, stale work.
7. Use save_memory to record important decisions or facts you discover. 7. Use save_memory to record decisions and facts you discover.
## Style ## Style
- Direct. No filler. - Direct. No filler. No "Great question!".
- Honest about uncertainty. - Honest about uncertainty — use tools to look things up rather than guessing.
- When spawning agents, be specific — give them full context, not vague instructions. - When spawning agents, be specific — full context, not vague instructions.
- Keep responses concise unless the user needs detail. - Concise unless detail is needed.
- Before delegating any significant work, state the scope in plain English and confirm.
## Security ## Security
- Never spawn agents on: mark/vibn-frontend, mark/theia-code-os, mark/vibn-agent-runner, mark/vibn-api, mark/master-ai - Never spawn agents on: mark/vibn-frontend, mark/theia-code-os, mark/vibn-agent-runner, mark/vibn-api, mark/master-ai
- Those are protected platform repos — read-only for you, not writable by agents. - Those are protected platform repos — read-only for awareness, never writable by agents.
{{knowledge}} {{knowledge}}
`.trim()); `.trim());

View File

@@ -97,10 +97,11 @@ app.get('/health', (_req: Request, res: Response) => {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
app.post('/api/mirror', async (req: Request, res: Response) => { app.post('/api/mirror', async (req: Request, res: Response) => {
const { github_url, gitea_repo, project_name } = req.body as { const { github_url, gitea_repo, project_name, github_token } = req.body as {
github_url?: string; github_url?: string;
gitea_repo?: string; // e.g. "mark/opsos" gitea_repo?: string; // e.g. "mark/opsos"
project_name?: string; project_name?: string;
github_token?: string; // PAT for private repos
}; };
if (!github_url || !gitea_repo) { if (!github_url || !gitea_repo) {
@@ -132,8 +133,14 @@ app.post('/api/mirror', async (req: Request, res: Response) => {
console.log(`[mirror] Cloning ${github_url}${tmpDir}`); console.log(`[mirror] Cloning ${github_url}${tmpDir}`);
fs.mkdirSync(tmpDir, { recursive: true }); fs.mkdirSync(tmpDir, { recursive: true });
// Build authenticated clone URL for private repos
let cloneUrl = github_url;
if (github_token) {
cloneUrl = github_url.replace('https://', `https://${github_token}@`);
}
// Mirror-clone the GitHub repo (preserves all branches + tags) // Mirror-clone the GitHub repo (preserves all branches + tags)
execSync(`git clone --mirror "${github_url}" "${tmpDir}/.git"`, { execSync(`git clone --mirror "${cloneUrl}" "${tmpDir}/.git"`, {
stdio: 'pipe', stdio: 'pipe',
timeout: 120_000 timeout: 120_000
}); });

48
src/vibn-events-ingest.ts Normal file
View File

@@ -0,0 +1,48 @@
/**
* Push structured timeline events to vibn-frontend (Postgres via ingest API).
* Complements PATCH output lines — enables SSE replay without polling every line.
*/
import { randomUUID } from 'crypto';
export interface IngestEventInput {
type: string;
payload?: Record<string, unknown>;
ts?: string;
}
export async function ingestSessionEvents(
vibnApiUrl: string,
projectId: string,
sessionId: string,
events: IngestEventInput[]
): Promise<void> {
if (events.length === 0) return;
const secret = process.env.AGENT_RUNNER_SECRET ?? '';
const base = vibnApiUrl.replace(/\/$/, '');
const url = `${base}/api/projects/${projectId}/agent/sessions/${sessionId}/events`;
try {
const res = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-agent-runner-secret': secret,
},
body: JSON.stringify({
events: events.map((e) => ({
clientEventId: randomUUID(),
ts: e.ts ?? new Date().toISOString(),
type: e.type,
payload: e.payload ?? {},
})),
}),
});
if (!res.ok) {
const t = await res.text();
console.warn('[ingest-events]', res.status, t.slice(0, 240));
}
} catch (err) {
console.warn('[ingest-events]', err instanceof Error ? err.message : err);
}
}