From e29dccf7452938eb4951bf2ab95cb92d68cdf5cb Mon Sep 17 00:00:00 2001 From: mawkone Date: Sun, 1 Mar 2026 15:38:42 -0800 Subject: [PATCH] refactor: implement three-layer agent architecture (agents / prompts / skills) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Layer 1 — src/agents/ (thin agent definitions, no prompt text) registry.ts — AgentConfig, registerAgent(), getAgent(), AGENTS proxy, pick() orchestrator.ts, coder.ts, pm.ts, marketing.ts — one file each, just metadata + tool picks index.ts — barrel: imports prompts then agents (correct registration order) Layer 2 — src/prompts/ (prompt text separated from agent logic) loader.ts — registerPrompt(), resolvePrompt() with {{variable}} substitution orchestrator.ts, coder.ts, pm.ts, marketing.ts — prompt templates as registered strings orchestrator.ts now uses resolvePrompt('orchestrator', { knowledge }) instead of inline SYSTEM_PROMPT const; {{knowledge}} variable injects project memory cleanly. agent-runner.ts uses resolvePrompt(config.promptId) per agent turn. Layer 3 — src/tools/skills.ts (new skills capability) list_skills(repo) — lists .skills//SKILL.md directories from a Gitea repo get_skill(repo, name) — reads and returns the markdown body of a skill file Orchestrator and all agents now have get_skill in their tool sets. Orchestrator also has list_skills and references skills in its prompt. Also fixed: - server.ts now passes history + knowledge_context from request body to orchestratorChat() (these were being sent by the frontend but silently dropped) - server.ts imports PROTECTED_GITEA_REPOS from tools/security.ts (no more duplicate) - Deleted src/agents.ts (replaced by src/agents/ directory) Made-with: Cursor --- dist/agent-runner.js | 4 +- dist/agents/coder.d.ts | 1 + dist/agents/coder.js | 16 ++++ dist/agents/index.d.ts | 9 +++ dist/agents/index.js | 19 +++++ dist/agents/marketing.d.ts | 1 + dist/agents/marketing.js | 14 ++++ dist/agents/orchestrator.d.ts | 1 + dist/agents/orchestrator.js | 17 +++++ dist/agents/pm.d.ts | 1 + dist/agents/pm.js | 15 ++++ dist/agents/registry.d.ts | 18 +++++ dist/agents/registry.js | 34 +++++++++ dist/orchestrator.js | 64 ++-------------- dist/prompts/coder.d.ts | 1 + dist/prompts/coder.js | 31 ++++++++ dist/prompts/loader.d.ts | 7 ++ dist/prompts/loader.js | 30 ++++++++ dist/prompts/marketing.d.ts | 1 + dist/prompts/marketing.js | 18 +++++ dist/prompts/orchestrator.d.ts | 1 + dist/prompts/orchestrator.js | 62 +++++++++++++++ dist/prompts/pm.d.ts | 1 + dist/prompts/pm.js | 20 +++++ dist/server.js | 18 ++--- dist/tools/index.d.ts | 1 + dist/tools/index.js | 1 + dist/tools/skills.d.ts | 1 + dist/tools/skills.js | 60 +++++++++++++++ src/agent-runner.ts | 4 +- src/agents.ts | 133 --------------------------------- src/agents/coder.ts | 15 ++++ src/agents/index.ts | 14 ++++ src/agents/marketing.ts | 13 ++++ src/agents/orchestrator.ts | 16 ++++ src/agents/pm.ts | 14 ++++ src/agents/registry.ts | 41 ++++++++++ src/orchestrator.ts | 67 +++-------------- src/prompts/coder.ts | 30 ++++++++ src/prompts/loader.ts | 28 +++++++ src/prompts/marketing.ts | 17 +++++ src/prompts/orchestrator.ts | 61 +++++++++++++++ src/prompts/pm.ts | 19 +++++ src/server.ts | 29 ++++--- src/tools/index.ts | 1 + src/tools/skills.ts | 62 +++++++++++++++ 46 files changed, 759 insertions(+), 272 deletions(-) create mode 100644 dist/agents/coder.d.ts create mode 100644 dist/agents/coder.js create mode 100644 dist/agents/index.d.ts create mode 100644 dist/agents/index.js create mode 100644 dist/agents/marketing.d.ts create mode 100644 dist/agents/marketing.js create mode 100644 dist/agents/orchestrator.d.ts create mode 100644 dist/agents/orchestrator.js create mode 100644 dist/agents/pm.d.ts create mode 100644 dist/agents/pm.js create mode 100644 dist/agents/registry.d.ts create mode 100644 dist/agents/registry.js create mode 100644 dist/prompts/coder.d.ts create mode 100644 dist/prompts/coder.js create mode 100644 dist/prompts/loader.d.ts create mode 100644 dist/prompts/loader.js create mode 100644 dist/prompts/marketing.d.ts create mode 100644 dist/prompts/marketing.js create mode 100644 dist/prompts/orchestrator.d.ts create mode 100644 dist/prompts/orchestrator.js create mode 100644 dist/prompts/pm.d.ts create mode 100644 dist/prompts/pm.js create mode 100644 dist/tools/skills.d.ts create mode 100644 dist/tools/skills.js delete mode 100644 src/agents.ts create mode 100644 src/agents/coder.ts create mode 100644 src/agents/index.ts create mode 100644 src/agents/marketing.ts create mode 100644 src/agents/orchestrator.ts create mode 100644 src/agents/pm.ts create mode 100644 src/agents/registry.ts create mode 100644 src/prompts/coder.ts create mode 100644 src/prompts/loader.ts create mode 100644 src/prompts/marketing.ts create mode 100644 src/prompts/orchestrator.ts create mode 100644 src/prompts/pm.ts create mode 100644 src/tools/skills.ts diff --git a/dist/agent-runner.js b/dist/agent-runner.js index 2464e85..ff17b7c 100644 --- a/dist/agent-runner.js +++ b/dist/agent-runner.js @@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true }); exports.runAgent = runAgent; const llm_1 = require("./llm"); const tools_1 = require("./tools"); +const loader_1 = require("./prompts/loader"); const job_store_1 = require("./job-store"); const MAX_TURNS = 40; /** @@ -23,8 +24,9 @@ async function runAgent(job, config, task, ctx) { (0, job_store_1.updateJob)(job.id, { status: 'running', progress: `Starting ${config.name} (${llm.modelId})…` }); while (turn < MAX_TURNS) { turn++; + const systemPrompt = (0, loader_1.resolvePrompt)(config.promptId); const messages = [ - { role: 'system', content: config.systemPrompt }, + { role: 'system', content: systemPrompt }, ...history ]; const response = await llm.chat(messages, oaiTools, 8192); diff --git a/dist/agents/coder.d.ts b/dist/agents/coder.d.ts new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/dist/agents/coder.d.ts @@ -0,0 +1 @@ +export {}; diff --git a/dist/agents/coder.js b/dist/agents/coder.js new file mode 100644 index 0000000..5820423 --- /dev/null +++ b/dist/agents/coder.js @@ -0,0 +1,16 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +const registry_1 = require("./registry"); +(0, registry_1.registerAgent)({ + name: 'Coder', + description: 'Senior software engineer — writes, edits, tests, commits, and pushes code', + model: 'B', + promptId: 'coder', + tools: (0, registry_1.pick)([ + 'read_file', 'write_file', 'replace_in_file', 'list_directory', 'find_files', 'search_code', + 'execute_command', + 'git_commit_and_push', + 'gitea_list_issues', 'gitea_close_issue', + 'get_skill' + ]) +}); diff --git a/dist/agents/index.d.ts b/dist/agents/index.d.ts new file mode 100644 index 0000000..aba5a4b --- /dev/null +++ b/dist/agents/index.d.ts @@ -0,0 +1,9 @@ +import '../prompts/orchestrator'; +import '../prompts/coder'; +import '../prompts/pm'; +import '../prompts/marketing'; +import './orchestrator'; +import './coder'; +import './pm'; +import './marketing'; +export { AgentConfig, AGENTS, getAgent, allAgents, pick } from './registry'; diff --git a/dist/agents/index.js b/dist/agents/index.js new file mode 100644 index 0000000..eb428b7 --- /dev/null +++ b/dist/agents/index.js @@ -0,0 +1,19 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.pick = exports.allAgents = exports.getAgent = exports.AGENTS = void 0; +// Import prompt templates first — side effects register them before agents reference promptIds +require("../prompts/orchestrator"); +require("../prompts/coder"); +require("../prompts/pm"); +require("../prompts/marketing"); +// Import agent files — side effects register each agent into the registry +require("./orchestrator"); +require("./coder"); +require("./pm"); +require("./marketing"); +// Re-export public API +var registry_1 = require("./registry"); +Object.defineProperty(exports, "AGENTS", { enumerable: true, get: function () { return registry_1.AGENTS; } }); +Object.defineProperty(exports, "getAgent", { enumerable: true, get: function () { return registry_1.getAgent; } }); +Object.defineProperty(exports, "allAgents", { enumerable: true, get: function () { return registry_1.allAgents; } }); +Object.defineProperty(exports, "pick", { enumerable: true, get: function () { return registry_1.pick; } }); diff --git a/dist/agents/marketing.d.ts b/dist/agents/marketing.d.ts new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/dist/agents/marketing.d.ts @@ -0,0 +1 @@ +export {}; diff --git a/dist/agents/marketing.js b/dist/agents/marketing.js new file mode 100644 index 0000000..45ebb6a --- /dev/null +++ b/dist/agents/marketing.js @@ -0,0 +1,14 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +const registry_1 = require("./registry"); +(0, registry_1.registerAgent)({ + name: 'Marketing', + description: 'Marketing specialist — copy, blog posts, release notes, landing page content', + model: 'A', + promptId: 'marketing', + tools: (0, registry_1.pick)([ + 'read_file', 'write_file', 'replace_in_file', 'list_directory', 'find_files', 'search_code', + 'git_commit_and_push', + 'get_skill' + ]) +}); diff --git a/dist/agents/orchestrator.d.ts b/dist/agents/orchestrator.d.ts new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/dist/agents/orchestrator.d.ts @@ -0,0 +1 @@ +export {}; diff --git a/dist/agents/orchestrator.js b/dist/agents/orchestrator.js new file mode 100644 index 0000000..1fc45d6 --- /dev/null +++ b/dist/agents/orchestrator.js @@ -0,0 +1,17 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +const registry_1 = require("./registry"); +(0, registry_1.registerAgent)({ + name: 'Orchestrator', + description: 'Master coordinator — breaks down goals and delegates to specialist agents', + model: 'B', + promptId: 'orchestrator', + tools: (0, registry_1.pick)([ + 'gitea_create_issue', 'gitea_list_issues', 'gitea_close_issue', + 'spawn_agent', 'get_job_status', + 'coolify_list_projects', 'coolify_list_applications', 'coolify_deploy', 'coolify_get_logs', + 'list_repos', 'list_all_issues', 'list_all_apps', 'get_app_status', + 'read_repo_file', 'deploy_app', 'save_memory', + 'list_skills', 'get_skill' + ]) +}); diff --git a/dist/agents/pm.d.ts b/dist/agents/pm.d.ts new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/dist/agents/pm.d.ts @@ -0,0 +1 @@ +export {}; diff --git a/dist/agents/pm.js b/dist/agents/pm.js new file mode 100644 index 0000000..27d824d --- /dev/null +++ b/dist/agents/pm.js @@ -0,0 +1,15 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +const registry_1 = require("./registry"); +(0, registry_1.registerAgent)({ + name: 'PM', + description: 'Product manager — docs, issue management, project health reports', + model: 'A', + promptId: 'pm', + tools: (0, registry_1.pick)([ + 'gitea_create_issue', 'gitea_list_issues', 'gitea_close_issue', + 'read_file', 'write_file', 'replace_in_file', 'list_directory', 'find_files', 'search_code', + 'git_commit_and_push', + 'get_skill' + ]) +}); diff --git a/dist/agents/registry.d.ts b/dist/agents/registry.d.ts new file mode 100644 index 0000000..75ab35a --- /dev/null +++ b/dist/agents/registry.d.ts @@ -0,0 +1,18 @@ +import { ToolDefinition } from '../tools'; +export interface AgentConfig { + name: string; + description: string; + model: string; + promptId: string; + tools: ToolDefinition[]; +} +export declare function registerAgent(config: AgentConfig): void; +export declare function getAgent(name: string): AgentConfig | undefined; +export declare function allAgents(): AgentConfig[]; +/** + * Backwards-compatible AGENTS object — populated as agents register. + * server.ts uses AGENTS[name] and Object.values(AGENTS). + */ +export declare const AGENTS: Record; +/** Pick tools from ALL_TOOLS by name. */ +export declare function pick(names: string[]): ToolDefinition[]; diff --git a/dist/agents/registry.js b/dist/agents/registry.js new file mode 100644 index 0000000..d95b8f9 --- /dev/null +++ b/dist/agents/registry.js @@ -0,0 +1,34 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.AGENTS = void 0; +exports.registerAgent = registerAgent; +exports.getAgent = getAgent; +exports.allAgents = allAgents; +exports.pick = pick; +const tools_1 = require("../tools"); +const _registry = new Map(); +function registerAgent(config) { + _registry.set(config.name, config); +} +function getAgent(name) { + return _registry.get(name); +} +function allAgents() { + return [..._registry.values()]; +} +/** + * Backwards-compatible AGENTS object — populated as agents register. + * server.ts uses AGENTS[name] and Object.values(AGENTS). + */ +exports.AGENTS = new Proxy({}, { + get(_target, prop) { return _registry.get(prop); }, + ownKeys() { return [..._registry.keys()]; }, + getOwnPropertyDescriptor(_target, prop) { + const v = _registry.get(prop); + return v ? { configurable: true, enumerable: true, value: v } : undefined; + } +}); +/** Pick tools from ALL_TOOLS by name. */ +function pick(names) { + return tools_1.ALL_TOOLS.filter(t => names.includes(t.name)); +} diff --git a/dist/orchestrator.js b/dist/orchestrator.js index 7a8866b..16f6926 100644 --- a/dist/orchestrator.js +++ b/dist/orchestrator.js @@ -5,6 +5,7 @@ exports.clearSession = clearSession; exports.orchestratorChat = orchestratorChat; const llm_1 = require("./llm"); const tools_1 = require("./tools"); +const loader_1 = require("./prompts/loader"); const MAX_TURNS = 20; const sessions = new Map(); function getOrCreateSession(sessionId) { @@ -32,59 +33,6 @@ function clearSession(sessionId) { sessions.delete(sessionId); } // --------------------------------------------------------------------------- -// Orchestrator system prompt -// --------------------------------------------------------------------------- -const SYSTEM_PROMPT = `You are the Master Orchestrator for Vibn — an AI-powered cloud development platform. - -You run continuously and have full awareness of the Vibn project. You can take autonomous action on behalf of the user. - -## What Vibn is -Vibn lets developers build products using AI agents: -- Frontend app (Next.js) at vibnai.com -- Backend API at api.vibnai.com -- Agent runner (this system) at agents.vibnai.com -- Cloud IDE (Theia) at 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 - -**Awareness** (understand current state first): -- list_repos — all Git repositories -- list_all_issues — open/in-progress work -- list_all_apps — deployed apps and their status -- get_app_status — health of a specific app -- read_repo_file — read any file from any repo without cloning - -**Action** (get things done): -- spawn_agent — dispatch Coder, PM, or Marketing agent on a repo -- get_job_status — check a running agent job -- deploy_app — trigger a Coolify deployment -- gitea_create_issue — track work (label agent:coder/pm/marketing to auto-trigger) -- gitea_list_issues / gitea_close_issue — issue lifecycle - -## Specialist agents you can spawn -- **Coder** — writes code, tests, commits, and pushes -- **PM** — docs, issues, sprint tracking -- **Marketing** — copy, release notes, blog posts - -## How you work -1. Use awareness tools first if you need current state. -2. Break the task into concrete steps. -3. Spawn the right agent(s) with specific, detailed instructions. -4. Track and report on results. -5. If you notice something that needs attention (failed deploy, open bugs, stale issues), mention it proactively. - -## Style -- Direct. No filler. -- Honest about uncertainty. -- When spawning agents, be specific — give them full context, not vague instructions. -- Keep responses concise unless the user needs detail. - -## Security -- 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.`; -// --------------------------------------------------------------------------- // Main orchestrator chat — uses GLM-5 (Tier B) by default // --------------------------------------------------------------------------- async function orchestratorChat(sessionId, userMessage, ctx, opts) { @@ -102,10 +50,12 @@ async function orchestratorChat(sessionId, userMessage, ctx, opts) { let finalReply = ''; let finalReasoning = null; const toolCallNames = []; - // Build system prompt — inject project knowledge if provided - const systemContent = opts?.knowledgeContext - ? `${SYSTEM_PROMPT}\n\n## Project Memory (known facts)\n${opts.knowledgeContext}` - : SYSTEM_PROMPT; + // Resolve system prompt from template — {{knowledge}} injects project memory + const systemContent = (0, loader_1.resolvePrompt)('orchestrator', { + knowledge: opts?.knowledgeContext + ? `## Project Memory (known facts)\n${opts.knowledgeContext}` + : '' + }); // Build messages with system prompt prepended; keep last 40 for cost control const buildMessages = () => [ { role: 'system', content: systemContent }, diff --git a/dist/prompts/coder.d.ts b/dist/prompts/coder.d.ts new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/dist/prompts/coder.d.ts @@ -0,0 +1 @@ +export {}; diff --git a/dist/prompts/coder.js b/dist/prompts/coder.js new file mode 100644 index 0000000..cab114b --- /dev/null +++ b/dist/prompts/coder.js @@ -0,0 +1,31 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +const loader_1 = require("./loader"); +(0, loader_1.registerPrompt)('coder', ` +You are an expert senior software engineer working autonomously on a Git repository. + +## Workflow +1. Explore the codebase: list_directory, find_files, read_file. +2. Search for patterns: search_code. +3. Plan your changes before making them. +4. Read every file BEFORE editing it. +5. Make changes: write_file for new files, replace_in_file for targeted edits. +6. Run tests/lint if applicable: execute_command. +7. Commit and push when complete: git_commit_and_push. + +## Code quality +- Match existing style exactly. +- No TODO comments — implement or skip. +- Write complete files, not partial snippets. +- Run tests and fix failures before committing. +- Commit messages: imperative mood, concise (e.g. "add user authentication"). + +## Safety +- Never delete files unless explicitly told to. +- Never touch .env files or credentials. +- Never commit secrets or API keys. + +If triggered by a Gitea issue: close it with gitea_close_issue after committing. + +{{skills}} +`.trim()); diff --git a/dist/prompts/loader.d.ts b/dist/prompts/loader.d.ts new file mode 100644 index 0000000..f813d17 --- /dev/null +++ b/dist/prompts/loader.d.ts @@ -0,0 +1,7 @@ +export declare function registerPrompt(id: string, template: string): void; +/** + * Resolve a prompt template by ID, substituting {{variable}} placeholders. + * Missing variables are replaced with an empty string. + */ +export declare function resolvePrompt(id: string, variables?: Record): string; +export declare function hasPrompt(id: string): boolean; diff --git a/dist/prompts/loader.js b/dist/prompts/loader.js new file mode 100644 index 0000000..26ebd39 --- /dev/null +++ b/dist/prompts/loader.js @@ -0,0 +1,30 @@ +"use strict"; +// --------------------------------------------------------------------------- +// Prompt registry + variable resolver +// +// Prompts are template strings stored in this directory, one file per agent. +// Variables are resolved at call time using {{variable_name}} syntax. +// +// Future: swap template strings for .md files with a build-time copy step. +// --------------------------------------------------------------------------- +Object.defineProperty(exports, "__esModule", { value: true }); +exports.registerPrompt = registerPrompt; +exports.resolvePrompt = resolvePrompt; +exports.hasPrompt = hasPrompt; +const _prompts = new Map(); +function registerPrompt(id, template) { + _prompts.set(id, template); +} +/** + * Resolve a prompt template by ID, substituting {{variable}} placeholders. + * Missing variables are replaced with an empty string. + */ +function resolvePrompt(id, variables = {}) { + const template = _prompts.get(id); + if (!template) + throw new Error(`Prompt not found: "${id}"`); + return template.replace(/\{\{(\w+)\}\}/g, (_, key) => variables[key] ?? ''); +} +function hasPrompt(id) { + return _prompts.has(id); +} diff --git a/dist/prompts/marketing.d.ts b/dist/prompts/marketing.d.ts new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/dist/prompts/marketing.d.ts @@ -0,0 +1 @@ +export {}; diff --git a/dist/prompts/marketing.js b/dist/prompts/marketing.js new file mode 100644 index 0000000..abdbb13 --- /dev/null +++ b/dist/prompts/marketing.js @@ -0,0 +1,18 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +const loader_1 = require("./loader"); +(0, loader_1.registerPrompt)('marketing', ` +You are an autonomous Marketing specialist for a SaaS product called Vibn. + +Vibn is a cloud-based AI-powered development environment that helps teams build faster with AI agents. + +## Responsibilities +1. Write landing page copy, emails, and social media content. +2. Write technical blog posts explaining features accessibly. +3. Write release notes that highlight user-facing value. +4. Maintain brand voice: smart, confident, practical. No hype, no jargon. + +Always create real files in the repo (e.g. blog/2026-02-release.md) and commit them. + +{{skills}} +`.trim()); diff --git a/dist/prompts/orchestrator.d.ts b/dist/prompts/orchestrator.d.ts new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/dist/prompts/orchestrator.d.ts @@ -0,0 +1 @@ +export {}; diff --git a/dist/prompts/orchestrator.js b/dist/prompts/orchestrator.js new file mode 100644 index 0000000..6f52533 --- /dev/null +++ b/dist/prompts/orchestrator.js @@ -0,0 +1,62 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +const loader_1 = require("./loader"); +(0, loader_1.registerPrompt)('orchestrator', ` +You are the Master Orchestrator for Vibn — an AI-powered cloud development platform. + +You run continuously and have full awareness of the Vibn project. You can take autonomous action on behalf of the user. + +## What Vibn is +Vibn lets developers build products using AI agents: +- Frontend app (Next.js) at vibnai.com +- Backend API at api.vibnai.com +- Agent runner (this system) at agents.vibnai.com +- Cloud IDE (Theia) at 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 + +**Awareness** (understand current state first): +- list_repos — all Git repositories +- list_all_issues — open/in-progress work +- list_all_apps — deployed apps and their status +- get_app_status — health of a specific app +- read_repo_file — read any file from any repo without cloning +- list_skills — list available skills for a project repo +- get_skill — read the full content of a specific skill + +**Action** (get things done): +- spawn_agent — dispatch Coder, PM, or Marketing agent on a repo +- get_job_status — check a running agent job +- deploy_app — trigger a Coolify deployment +- gitea_create_issue — track work (label agent:coder/pm/marketing to auto-trigger) +- gitea_list_issues / gitea_close_issue — issue lifecycle +- save_memory — persist important project facts across conversations + +## Specialist agents you can spawn +- **Coder** — writes code, tests, commits, and pushes +- **PM** — docs, issues, sprint tracking +- **Marketing** — copy, release notes, blog posts + +## How you work +1. Use awareness tools first if you need current state. +2. Break the task into concrete steps. +3. Before spawning an agent, call list_skills to check if relevant skills exist and pass them as context. +4. Spawn the right agent(s) with specific, detailed instructions. +5. Track and report on results. +6. If you notice something that needs attention (failed deploy, open bugs, stale issues), mention it proactively. +7. Use save_memory to record important decisions or facts you discover. + +## Style +- Direct. No filler. +- Honest about uncertainty. +- When spawning agents, be specific — give them full context, not vague instructions. +- Keep responses concise unless the user needs detail. + +## Security +- 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. + +{{knowledge}} +`.trim()); diff --git a/dist/prompts/pm.d.ts b/dist/prompts/pm.d.ts new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/dist/prompts/pm.d.ts @@ -0,0 +1 @@ +export {}; diff --git a/dist/prompts/pm.js b/dist/prompts/pm.js new file mode 100644 index 0000000..c0b30a6 --- /dev/null +++ b/dist/prompts/pm.js @@ -0,0 +1,20 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +const loader_1 = require("./loader"); +(0, loader_1.registerPrompt)('pm', ` +You are an autonomous Product Manager for a software project hosted on Gitea. + +## Responsibilities +1. Create, update, and close Gitea issues. +2. Write and update docs in the repository. +3. Summarize project state and create reports. +4. Triage bugs and features by impact. + +## When writing docs +- Clear and concise. +- Markdown formatting. +- Keep docs in sync with the codebase. +- Always commit after writing. + +{{skills}} +`.trim()); diff --git a/dist/server.js b/dist/server.js index 99471b2..3292f67 100644 --- a/dist/server.js +++ b/dist/server.js @@ -45,15 +45,8 @@ const child_process_1 = require("child_process"); const job_store_1 = require("./job-store"); const agent_runner_1 = require("./agent-runner"); const agents_1 = require("./agents"); +const security_1 = require("./tools/security"); const orchestrator_1 = require("./orchestrator"); -// Protected Vibn platform repos — agents cannot clone or work in these workspaces -const PROTECTED_GITEA_REPOS = new Set([ - 'mark/vibn-frontend', - 'mark/theia-code-os', - 'mark/vibn-agent-runner', - 'mark/vibn-api', - 'mark/master-ai', -]); const app = (0, express_1.default)(); app.use((0, cors_1.default)()); const startTime = new Date(); @@ -71,7 +64,7 @@ function ensureWorkspace(repo) { fs.mkdirSync(dir, { recursive: true }); return dir; } - if (PROTECTED_GITEA_REPOS.has(repo)) { + if (security_1.PROTECTED_GITEA_REPOS.has(repo)) { throw new Error(`SECURITY: Repo "${repo}" is a protected Vibn platform repo. ` + `Agents cannot clone or work in this workspace.`); } @@ -196,7 +189,7 @@ app.get('/api/jobs/:id', (req, res) => { // Orchestrator — persistent chat with full project context // --------------------------------------------------------------------------- app.post('/orchestrator/chat', async (req, res) => { - const { message, session_id } = req.body; + const { message, session_id, history, knowledge_context } = req.body; if (!message) { res.status(400).json({ error: '"message" is required' }); return; @@ -204,7 +197,10 @@ app.post('/orchestrator/chat', async (req, res) => { const sessionId = session_id || `session_${Date.now()}`; const ctx = buildContext(); try { - const result = await (0, orchestrator_1.orchestratorChat)(sessionId, message, ctx); + const result = await (0, orchestrator_1.orchestratorChat)(sessionId, message, ctx, { + preloadedHistory: history, + knowledgeContext: knowledge_context + }); res.json(result); } catch (err) { diff --git a/dist/tools/index.d.ts b/dist/tools/index.d.ts index 2d23dcd..b1e11c0 100644 --- a/dist/tools/index.d.ts +++ b/dist/tools/index.d.ts @@ -5,6 +5,7 @@ import './gitea'; import './coolify'; import './agent'; import './memory'; +import './skills'; export { ALL_TOOLS, executeTool, ToolDefinition } from './registry'; export { ToolContext, MemoryUpdate } from './context'; export { PROTECTED_GITEA_REPOS, PROTECTED_COOLIFY_PROJECT, PROTECTED_COOLIFY_APPS, assertGiteaWritable, assertCoolifyDeployable } from './security'; diff --git a/dist/tools/index.js b/dist/tools/index.js index aa672ca..34de926 100644 --- a/dist/tools/index.js +++ b/dist/tools/index.js @@ -10,6 +10,7 @@ require("./gitea"); require("./coolify"); require("./agent"); require("./memory"); +require("./skills"); // Re-export the public API — identical surface to the old tools.ts var registry_1 = require("./registry"); Object.defineProperty(exports, "ALL_TOOLS", { enumerable: true, get: function () { return registry_1.ALL_TOOLS; } }); diff --git a/dist/tools/skills.d.ts b/dist/tools/skills.d.ts new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/dist/tools/skills.d.ts @@ -0,0 +1 @@ +export {}; diff --git a/dist/tools/skills.js b/dist/tools/skills.js new file mode 100644 index 0000000..7b7aada --- /dev/null +++ b/dist/tools/skills.js @@ -0,0 +1,60 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +const registry_1 = require("./registry"); +const SKILL_FILE = 'SKILL.md'; +const SKILLS_DIR = '.skills'; +async function giteaGetContents(repo, path, ctx) { + const res = await fetch(`${ctx.gitea.apiUrl}/api/v1/repos/${repo}/contents/${path}`, { + headers: { 'Authorization': `token ${ctx.gitea.apiToken}` } + }); + if (!res.ok) + return null; + return res.json(); +} +(0, registry_1.registerTool)({ + name: 'list_skills', + description: `List available skills for a project repo. Skills are stored in .skills//SKILL.md and provide reusable instructions the agent should follow (e.g. deploy process, test commands, code conventions).`, + parameters: { + type: 'object', + properties: { + repo: { type: 'string', description: 'Repo in "owner/name" format' } + }, + required: ['repo'] + }, + async handler(args, ctx) { + const repo = String(args.repo); + const contents = await giteaGetContents(repo, SKILLS_DIR, ctx); + if (!contents || !Array.isArray(contents)) { + return { skills: [], message: `No .skills/ directory found in ${repo}` }; + } + const skills = contents + .filter((entry) => entry.type === 'dir') + .map((entry) => ({ name: entry.name, path: entry.path })); + return { repo, skills }; + } +}); +(0, registry_1.registerTool)({ + name: 'get_skill', + description: `Read the full content of a specific skill from a project repo. Call list_skills first to see what's available. Use this before spawning agents so they have the relevant project-specific instructions.`, + parameters: { + type: 'object', + properties: { + repo: { type: 'string', description: 'Repo in "owner/name" format' }, + skill_name: { type: 'string', description: 'Skill name (directory name inside .skills/)' } + }, + required: ['repo', 'skill_name'] + }, + async handler(args, ctx) { + const repo = String(args.repo); + const skillName = String(args.skill_name); + const filePath = `${SKILLS_DIR}/${skillName}/${SKILL_FILE}`; + const file = await giteaGetContents(repo, filePath, ctx); + if (!file || !file.content) { + return { error: `Skill "${skillName}" not found in ${repo}. Try list_skills to see available skills.` }; + } + const content = Buffer.from(file.content, 'base64').toString('utf8'); + // Strip YAML frontmatter if present, return just the markdown body + const body = content.replace(/^---[\s\S]*?---\s*/m, '').trim(); + return { repo, skill: skillName, content: body }; + } +}); diff --git a/src/agent-runner.ts b/src/agent-runner.ts index 3230c56..958429a 100644 --- a/src/agent-runner.ts +++ b/src/agent-runner.ts @@ -1,6 +1,7 @@ import { createLLM, toOAITools, LLMMessage } from './llm'; import { AgentConfig } from './agents'; import { executeTool, ToolContext } from './tools'; +import { resolvePrompt } from './prompts/loader'; import { Job, updateJob } from './job-store'; const MAX_TURNS = 40; @@ -40,8 +41,9 @@ export async function runAgent( while (turn < MAX_TURNS) { turn++; + const systemPrompt = resolvePrompt(config.promptId); const messages: LLMMessage[] = [ - { role: 'system', content: config.systemPrompt }, + { role: 'system', content: systemPrompt }, ...history ]; diff --git a/src/agents.ts b/src/agents.ts deleted file mode 100644 index ea2cec9..0000000 --- a/src/agents.ts +++ /dev/null @@ -1,133 +0,0 @@ -import { ToolDefinition, ALL_TOOLS } from './tools'; - -// --------------------------------------------------------------------------- -// Agent configuration -// --------------------------------------------------------------------------- - -export interface AgentConfig { - name: string; - description: string; - model: string; // model ID or tier ('A' | 'B' | 'C') - systemPrompt: string; - tools: ToolDefinition[]; -} - -const FILE_TOOLS = ['read_file', 'write_file', 'replace_in_file', 'list_directory', 'find_files', 'search_code']; -const SHELL_TOOLS = ['execute_command']; -const GIT_TOOLS = ['git_commit_and_push']; -const COOLIFY_TOOLS = ['coolify_list_projects', 'coolify_list_applications', 'coolify_deploy', 'coolify_get_logs']; -const GITEA_TOOLS = ['gitea_create_issue', 'gitea_list_issues', 'gitea_close_issue']; -const SPAWN_TOOL = ['spawn_agent']; - -function pick(names: string[]): ToolDefinition[] { - return ALL_TOOLS.filter(t => names.includes(t.name)); -} - -// --------------------------------------------------------------------------- -// Agent definitions -// -// model is a tier ('A' | 'B' | 'C') or a specific model ID. -// Tiers resolve at runtime via TIER_A_MODEL / TIER_B_MODEL / TIER_C_MODEL env vars. -// -// Tier A = gemini-2.5-flash — fast, cheap: routing, summaries, monitoring -// Tier B = zai-org/glm-5-maas — workhorse coding model -// Tier C = zai-org/glm-5-maas — complex decisions (or Claude Sonnet via TIER_C_MODEL) -// --------------------------------------------------------------------------- - -export const AGENTS: Record = { - - Orchestrator: { - name: 'Orchestrator', - description: 'Master coordinator — breaks down goals and delegates to specialist agents', - model: 'B', // GLM-5 — good planner, chain-of-thought reasoning - systemPrompt: `You are the Orchestrator for Vibn, an autonomous AI platform for software development. - -Your role: -1. Understand the high-level goal. -2. Break it into concrete sub-tasks. -3. Delegate to the right specialist agents via spawn_agent. -4. Track progress via Gitea issues. -5. Summarize results when done. - -Agents available: -- Coder: code changes, features, bug fixes, tests. -- PM: issue triage, docs, sprint planning. -- Marketing: copy, blog posts, release notes. - -Rules: -- Create a Gitea issue first to track the work. -- Delegate one agent at a time unless tasks are fully independent. -- Never write code yourself — delegate to Coder. -- Be specific in task descriptions when spawning agents.`, - tools: pick([...GITEA_TOOLS, ...SPAWN_TOOL, ...COOLIFY_TOOLS]) - }, - - Coder: { - name: 'Coder', - description: 'Senior software engineer — writes, edits, tests, commits, and pushes code', - model: 'B', // GLM-5 — strong at code generation and diffs - systemPrompt: `You are an expert senior software engineer working autonomously on a Git repository. - -Workflow: -1. Explore the codebase: list_directory, find_files, read_file. -2. Search for patterns: search_code. -3. Plan your changes before making them. -4. Read every file BEFORE editing it. -5. Make changes: write_file for new files, replace_in_file for targeted edits. -6. Run tests/lint if applicable: execute_command. -7. Commit and push when complete: git_commit_and_push. - -Code quality: -- Match existing style exactly. -- No TODO comments — implement or skip. -- Write complete files, not partial snippets. -- Run tests and fix failures before committing. -- Commit messages: imperative mood, concise (e.g. "add user authentication"). - -Safety: -- Never delete files unless explicitly told to. -- Never touch .env files or credentials. -- Never commit secrets or API keys. - -If triggered by a Gitea issue: close it with gitea_close_issue after committing.`, - tools: pick([...FILE_TOOLS, ...SHELL_TOOLS, ...GIT_TOOLS, ...GITEA_TOOLS]) - }, - - PM: { - name: 'PM', - description: 'Product manager — docs, issue management, project health reports', - model: 'A', // Gemini Flash — lightweight, cheap for docs/issue work - systemPrompt: `You are an autonomous Product Manager for a software project hosted on Gitea. - -Responsibilities: -1. Create, update, and close Gitea issues. -2. Write and update docs in the repository. -3. Summarize project state and create reports. -4. Triage bugs and features by impact. - -When writing docs: -- Clear and concise. -- Markdown formatting. -- Keep docs in sync with the codebase. -- Always commit after writing.`, - tools: pick([...GITEA_TOOLS, ...FILE_TOOLS, ...GIT_TOOLS]) - }, - - Marketing: { - name: 'Marketing', - description: 'Marketing specialist — copy, blog posts, release notes, landing page content', - model: 'A', // Gemini Flash — cheap for content generation - systemPrompt: `You are an autonomous Marketing specialist for a SaaS product called Vibn. - -Vibn is a cloud-based AI-powered development environment that helps teams build faster with AI agents. - -Responsibilities: -1. Write landing page copy, emails, and social media content. -2. Write technical blog posts explaining features accessibly. -3. Write release notes that highlight user-facing value. -4. Maintain brand voice: smart, confident, practical. No hype, no jargon. - -Always create real files in the repo (e.g. blog/2026-02-release.md) and commit them.`, - tools: pick([...FILE_TOOLS, ...GIT_TOOLS]) - } -}; diff --git a/src/agents/coder.ts b/src/agents/coder.ts new file mode 100644 index 0000000..64fb873 --- /dev/null +++ b/src/agents/coder.ts @@ -0,0 +1,15 @@ +import { registerAgent, pick } from './registry'; + +registerAgent({ + name: 'Coder', + description: 'Senior software engineer — writes, edits, tests, commits, and pushes code', + model: 'B', + promptId: 'coder', + tools: pick([ + 'read_file', 'write_file', 'replace_in_file', 'list_directory', 'find_files', 'search_code', + 'execute_command', + 'git_commit_and_push', + 'gitea_list_issues', 'gitea_close_issue', + 'get_skill' + ]) +}); diff --git a/src/agents/index.ts b/src/agents/index.ts new file mode 100644 index 0000000..1cdd383 --- /dev/null +++ b/src/agents/index.ts @@ -0,0 +1,14 @@ +// Import prompt templates first — side effects register them before agents reference promptIds +import '../prompts/orchestrator'; +import '../prompts/coder'; +import '../prompts/pm'; +import '../prompts/marketing'; + +// Import agent files — side effects register each agent into the registry +import './orchestrator'; +import './coder'; +import './pm'; +import './marketing'; + +// Re-export public API +export { AgentConfig, AGENTS, getAgent, allAgents, pick } from './registry'; diff --git a/src/agents/marketing.ts b/src/agents/marketing.ts new file mode 100644 index 0000000..55e25fc --- /dev/null +++ b/src/agents/marketing.ts @@ -0,0 +1,13 @@ +import { registerAgent, pick } from './registry'; + +registerAgent({ + name: 'Marketing', + description: 'Marketing specialist — copy, blog posts, release notes, landing page content', + model: 'A', + promptId: 'marketing', + tools: pick([ + 'read_file', 'write_file', 'replace_in_file', 'list_directory', 'find_files', 'search_code', + 'git_commit_and_push', + 'get_skill' + ]) +}); diff --git a/src/agents/orchestrator.ts b/src/agents/orchestrator.ts new file mode 100644 index 0000000..42b9bee --- /dev/null +++ b/src/agents/orchestrator.ts @@ -0,0 +1,16 @@ +import { registerAgent, pick } from './registry'; + +registerAgent({ + name: 'Orchestrator', + description: 'Master coordinator — breaks down goals and delegates to specialist agents', + model: 'B', + promptId: 'orchestrator', + tools: pick([ + 'gitea_create_issue', 'gitea_list_issues', 'gitea_close_issue', + 'spawn_agent', 'get_job_status', + 'coolify_list_projects', 'coolify_list_applications', 'coolify_deploy', 'coolify_get_logs', + 'list_repos', 'list_all_issues', 'list_all_apps', 'get_app_status', + 'read_repo_file', 'deploy_app', 'save_memory', + 'list_skills', 'get_skill' + ]) +}); diff --git a/src/agents/pm.ts b/src/agents/pm.ts new file mode 100644 index 0000000..c6914e5 --- /dev/null +++ b/src/agents/pm.ts @@ -0,0 +1,14 @@ +import { registerAgent, pick } from './registry'; + +registerAgent({ + name: 'PM', + description: 'Product manager — docs, issue management, project health reports', + model: 'A', + promptId: 'pm', + tools: pick([ + 'gitea_create_issue', 'gitea_list_issues', 'gitea_close_issue', + 'read_file', 'write_file', 'replace_in_file', 'list_directory', 'find_files', 'search_code', + 'git_commit_and_push', + 'get_skill' + ]) +}); diff --git a/src/agents/registry.ts b/src/agents/registry.ts new file mode 100644 index 0000000..45a86e3 --- /dev/null +++ b/src/agents/registry.ts @@ -0,0 +1,41 @@ +import { ToolDefinition, ALL_TOOLS } from '../tools'; + +export interface AgentConfig { + name: string; + description: string; + model: string; // tier ('A' | 'B' | 'C') or specific model ID + promptId: string; // key into the prompt registry (src/prompts/.ts) + tools: ToolDefinition[]; +} + +const _registry = new Map(); + +export function registerAgent(config: AgentConfig): void { + _registry.set(config.name, config); +} + +export function getAgent(name: string): AgentConfig | undefined { + return _registry.get(name); +} + +export function allAgents(): AgentConfig[] { + return [..._registry.values()]; +} + +/** + * Backwards-compatible AGENTS object — populated as agents register. + * server.ts uses AGENTS[name] and Object.values(AGENTS). + */ +export const AGENTS: Record = new Proxy({} as Record, { + get(_target, prop: string) { return _registry.get(prop); }, + ownKeys() { return [..._registry.keys()]; }, + getOwnPropertyDescriptor(_target, prop: string) { + const v = _registry.get(prop); + return v ? { configurable: true, enumerable: true, value: v } : undefined; + } +}); + +/** Pick tools from ALL_TOOLS by name. */ +export function pick(names: string[]): ToolDefinition[] { + return ALL_TOOLS.filter(t => names.includes(t.name)); +} diff --git a/src/orchestrator.ts b/src/orchestrator.ts index 75baef4..01fa801 100644 --- a/src/orchestrator.ts +++ b/src/orchestrator.ts @@ -1,5 +1,6 @@ import { createLLM, toOAITools, LLMMessage } from './llm'; import { ALL_TOOLS, executeTool, ToolContext, MemoryUpdate } from './tools'; +import { resolvePrompt } from './prompts/loader'; const MAX_TURNS = 20; @@ -43,60 +44,8 @@ export function clearSession(sessionId: string) { sessions.delete(sessionId); } -// --------------------------------------------------------------------------- -// Orchestrator system prompt -// --------------------------------------------------------------------------- - -const SYSTEM_PROMPT = `You are the Master Orchestrator for Vibn — an AI-powered cloud development platform. - -You run continuously and have full awareness of the Vibn project. You can take autonomous action on behalf of the user. - -## What Vibn is -Vibn lets developers build products using AI agents: -- Frontend app (Next.js) at vibnai.com -- Backend API at api.vibnai.com -- Agent runner (this system) at agents.vibnai.com -- Cloud IDE (Theia) at 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 - -**Awareness** (understand current state first): -- list_repos — all Git repositories -- list_all_issues — open/in-progress work -- list_all_apps — deployed apps and their status -- get_app_status — health of a specific app -- read_repo_file — read any file from any repo without cloning - -**Action** (get things done): -- spawn_agent — dispatch Coder, PM, or Marketing agent on a repo -- get_job_status — check a running agent job -- deploy_app — trigger a Coolify deployment -- gitea_create_issue — track work (label agent:coder/pm/marketing to auto-trigger) -- gitea_list_issues / gitea_close_issue — issue lifecycle - -## Specialist agents you can spawn -- **Coder** — writes code, tests, commits, and pushes -- **PM** — docs, issues, sprint tracking -- **Marketing** — copy, release notes, blog posts - -## How you work -1. Use awareness tools first if you need current state. -2. Break the task into concrete steps. -3. Spawn the right agent(s) with specific, detailed instructions. -4. Track and report on results. -5. If you notice something that needs attention (failed deploy, open bugs, stale issues), mention it proactively. - -## Style -- Direct. No filler. -- Honest about uncertainty. -- When spawning agents, be specific — give them full context, not vague instructions. -- Keep responses concise unless the user needs detail. - -## Security -- 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.`; +// Prompt text lives in src/prompts/orchestrator.ts — imported via agents/index.ts +// which is loaded before orchestratorChat() is first called. // --------------------------------------------------------------------------- // Chat types @@ -150,10 +99,12 @@ export async function orchestratorChat( let finalReasoning: string | null = null; const toolCallNames: string[] = []; - // Build system prompt — inject project knowledge if provided - const systemContent = opts?.knowledgeContext - ? `${SYSTEM_PROMPT}\n\n## Project Memory (known facts)\n${opts.knowledgeContext}` - : SYSTEM_PROMPT; + // Resolve system prompt from template — {{knowledge}} injects project memory + const systemContent = resolvePrompt('orchestrator', { + knowledge: opts?.knowledgeContext + ? `## Project Memory (known facts)\n${opts.knowledgeContext}` + : '' + }); // Build messages with system prompt prepended; keep last 40 for cost control const buildMessages = (): LLMMessage[] => [ diff --git a/src/prompts/coder.ts b/src/prompts/coder.ts new file mode 100644 index 0000000..cb668e9 --- /dev/null +++ b/src/prompts/coder.ts @@ -0,0 +1,30 @@ +import { registerPrompt } from './loader'; + +registerPrompt('coder', ` +You are an expert senior software engineer working autonomously on a Git repository. + +## Workflow +1. Explore the codebase: list_directory, find_files, read_file. +2. Search for patterns: search_code. +3. Plan your changes before making them. +4. Read every file BEFORE editing it. +5. Make changes: write_file for new files, replace_in_file for targeted edits. +6. Run tests/lint if applicable: execute_command. +7. Commit and push when complete: git_commit_and_push. + +## Code quality +- Match existing style exactly. +- No TODO comments — implement or skip. +- Write complete files, not partial snippets. +- Run tests and fix failures before committing. +- Commit messages: imperative mood, concise (e.g. "add user authentication"). + +## Safety +- Never delete files unless explicitly told to. +- Never touch .env files or credentials. +- Never commit secrets or API keys. + +If triggered by a Gitea issue: close it with gitea_close_issue after committing. + +{{skills}} +`.trim()); diff --git a/src/prompts/loader.ts b/src/prompts/loader.ts new file mode 100644 index 0000000..d645255 --- /dev/null +++ b/src/prompts/loader.ts @@ -0,0 +1,28 @@ +// --------------------------------------------------------------------------- +// Prompt registry + variable resolver +// +// Prompts are template strings stored in this directory, one file per agent. +// Variables are resolved at call time using {{variable_name}} syntax. +// +// Future: swap template strings for .md files with a build-time copy step. +// --------------------------------------------------------------------------- + +const _prompts = new Map(); + +export function registerPrompt(id: string, template: string): void { + _prompts.set(id, template); +} + +/** + * Resolve a prompt template by ID, substituting {{variable}} placeholders. + * Missing variables are replaced with an empty string. + */ +export function resolvePrompt(id: string, variables: Record = {}): string { + const template = _prompts.get(id); + if (!template) throw new Error(`Prompt not found: "${id}"`); + return template.replace(/\{\{(\w+)\}\}/g, (_, key) => variables[key] ?? ''); +} + +export function hasPrompt(id: string): boolean { + return _prompts.has(id); +} diff --git a/src/prompts/marketing.ts b/src/prompts/marketing.ts new file mode 100644 index 0000000..ba091f0 --- /dev/null +++ b/src/prompts/marketing.ts @@ -0,0 +1,17 @@ +import { registerPrompt } from './loader'; + +registerPrompt('marketing', ` +You are an autonomous Marketing specialist for a SaaS product called Vibn. + +Vibn is a cloud-based AI-powered development environment that helps teams build faster with AI agents. + +## Responsibilities +1. Write landing page copy, emails, and social media content. +2. Write technical blog posts explaining features accessibly. +3. Write release notes that highlight user-facing value. +4. Maintain brand voice: smart, confident, practical. No hype, no jargon. + +Always create real files in the repo (e.g. blog/2026-02-release.md) and commit them. + +{{skills}} +`.trim()); diff --git a/src/prompts/orchestrator.ts b/src/prompts/orchestrator.ts new file mode 100644 index 0000000..29a5d3c --- /dev/null +++ b/src/prompts/orchestrator.ts @@ -0,0 +1,61 @@ +import { registerPrompt } from './loader'; + +registerPrompt('orchestrator', ` +You are the Master Orchestrator for Vibn — an AI-powered cloud development platform. + +You run continuously and have full awareness of the Vibn project. You can take autonomous action on behalf of the user. + +## What Vibn is +Vibn lets developers build products using AI agents: +- Frontend app (Next.js) at vibnai.com +- Backend API at api.vibnai.com +- Agent runner (this system) at agents.vibnai.com +- Cloud IDE (Theia) at 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 + +**Awareness** (understand current state first): +- list_repos — all Git repositories +- list_all_issues — open/in-progress work +- list_all_apps — deployed apps and their status +- get_app_status — health of a specific app +- read_repo_file — read any file from any repo without cloning +- list_skills — list available skills for a project repo +- get_skill — read the full content of a specific skill + +**Action** (get things done): +- spawn_agent — dispatch Coder, PM, or Marketing agent on a repo +- get_job_status — check a running agent job +- deploy_app — trigger a Coolify deployment +- gitea_create_issue — track work (label agent:coder/pm/marketing to auto-trigger) +- gitea_list_issues / gitea_close_issue — issue lifecycle +- save_memory — persist important project facts across conversations + +## Specialist agents you can spawn +- **Coder** — writes code, tests, commits, and pushes +- **PM** — docs, issues, sprint tracking +- **Marketing** — copy, release notes, blog posts + +## How you work +1. Use awareness tools first if you need current state. +2. Break the task into concrete steps. +3. Before spawning an agent, call list_skills to check if relevant skills exist and pass them as context. +4. Spawn the right agent(s) with specific, detailed instructions. +5. Track and report on results. +6. If you notice something that needs attention (failed deploy, open bugs, stale issues), mention it proactively. +7. Use save_memory to record important decisions or facts you discover. + +## Style +- Direct. No filler. +- Honest about uncertainty. +- When spawning agents, be specific — give them full context, not vague instructions. +- Keep responses concise unless the user needs detail. + +## Security +- 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. + +{{knowledge}} +`.trim()); diff --git a/src/prompts/pm.ts b/src/prompts/pm.ts new file mode 100644 index 0000000..0771c57 --- /dev/null +++ b/src/prompts/pm.ts @@ -0,0 +1,19 @@ +import { registerPrompt } from './loader'; + +registerPrompt('pm', ` +You are an autonomous Product Manager for a software project hosted on Gitea. + +## Responsibilities +1. Create, update, and close Gitea issues. +2. Write and update docs in the repository. +3. Summarize project state and create reports. +4. Triage bugs and features by impact. + +## When writing docs +- Clear and concise. +- Markdown formatting. +- Keep docs in sync with the codebase. +- Always commit after writing. + +{{skills}} +`.trim()); diff --git a/src/server.ts b/src/server.ts index 040642e..687f24b 100644 --- a/src/server.ts +++ b/src/server.ts @@ -8,16 +8,9 @@ import { createJob, getJob, listJobs, updateJob } from './job-store'; import { runAgent } from './agent-runner'; import { AGENTS } from './agents'; import { ToolContext } from './tools'; +import { PROTECTED_GITEA_REPOS } from './tools/security'; import { orchestratorChat, listSessions, clearSession } from './orchestrator'; - -// Protected Vibn platform repos — agents cannot clone or work in these workspaces -const PROTECTED_GITEA_REPOS = new Set([ - 'mark/vibn-frontend', - 'mark/theia-code-os', - 'mark/vibn-agent-runner', - 'mark/vibn-api', - 'mark/master-ai', -]); +import { LLMMessage } from './llm'; const app = express(); app.use(cors()); @@ -186,14 +179,28 @@ app.get('/api/jobs/:id', (req: Request, res: Response) => { // --------------------------------------------------------------------------- app.post('/orchestrator/chat', async (req: Request, res: Response) => { - const { message, session_id } = req.body as { message?: string; session_id?: string }; + const { + message, + session_id, + history, + knowledge_context + } = req.body as { + message?: string; + session_id?: string; + history?: LLMMessage[]; + knowledge_context?: string; + }; + if (!message) { res.status(400).json({ error: '"message" is required' }); return; } const sessionId = session_id || `session_${Date.now()}`; const ctx = buildContext(); try { - const result = await orchestratorChat(sessionId, message, ctx); + const result = await orchestratorChat(sessionId, message, ctx, { + preloadedHistory: history, + knowledgeContext: knowledge_context + }); res.json(result); } catch (err) { res.status(500).json({ error: err instanceof Error ? err.message : String(err) }); diff --git a/src/tools/index.ts b/src/tools/index.ts index 20e9748..4450e81 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -7,6 +7,7 @@ import './gitea'; import './coolify'; import './agent'; import './memory'; +import './skills'; // Re-export the public API — identical surface to the old tools.ts export { ALL_TOOLS, executeTool, ToolDefinition } from './registry'; diff --git a/src/tools/skills.ts b/src/tools/skills.ts new file mode 100644 index 0000000..ad329e8 --- /dev/null +++ b/src/tools/skills.ts @@ -0,0 +1,62 @@ +import { registerTool } from './registry'; +import { ToolContext } from './context'; + +const SKILL_FILE = 'SKILL.md'; +const SKILLS_DIR = '.skills'; + +async function giteaGetContents(repo: string, path: string, ctx: ToolContext): Promise { + const res = await fetch(`${ctx.gitea.apiUrl}/api/v1/repos/${repo}/contents/${path}`, { + headers: { 'Authorization': `token ${ctx.gitea.apiToken}` } + }); + if (!res.ok) return null; + return res.json(); +} + +registerTool({ + name: 'list_skills', + description: `List available skills for a project repo. Skills are stored in .skills//SKILL.md and provide reusable instructions the agent should follow (e.g. deploy process, test commands, code conventions).`, + parameters: { + type: 'object', + properties: { + repo: { type: 'string', description: 'Repo in "owner/name" format' } + }, + required: ['repo'] + }, + async handler(args, ctx) { + const repo = String(args.repo); + const contents = await giteaGetContents(repo, SKILLS_DIR, ctx); + if (!contents || !Array.isArray(contents)) { + return { skills: [], message: `No .skills/ directory found in ${repo}` }; + } + const skills = contents + .filter((entry: any) => entry.type === 'dir') + .map((entry: any) => ({ name: entry.name, path: entry.path })); + return { repo, skills }; + } +}); + +registerTool({ + name: 'get_skill', + description: `Read the full content of a specific skill from a project repo. Call list_skills first to see what's available. Use this before spawning agents so they have the relevant project-specific instructions.`, + parameters: { + type: 'object', + properties: { + repo: { type: 'string', description: 'Repo in "owner/name" format' }, + skill_name: { type: 'string', description: 'Skill name (directory name inside .skills/)' } + }, + required: ['repo', 'skill_name'] + }, + async handler(args, ctx) { + const repo = String(args.repo); + const skillName = String(args.skill_name); + const filePath = `${SKILLS_DIR}/${skillName}/${SKILL_FILE}`; + const file = await giteaGetContents(repo, filePath, ctx); + if (!file || !file.content) { + return { error: `Skill "${skillName}" not found in ${repo}. Try list_skills to see available skills.` }; + } + const content = Buffer.from(file.content, 'base64').toString('utf8'); + // Strip YAML frontmatter if present, return just the markdown body + const body = content.replace(/^---[\s\S]*?---\s*/m, '').trim(); + return { repo, skill: skillName, content: body }; + } +});