refactor: implement three-layer agent architecture (agents / prompts / skills)
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/<name>/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
This commit is contained in:
4
dist/agent-runner.js
vendored
4
dist/agent-runner.js
vendored
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|||||||
exports.runAgent = runAgent;
|
exports.runAgent = runAgent;
|
||||||
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 job_store_1 = require("./job-store");
|
const job_store_1 = require("./job-store");
|
||||||
const MAX_TURNS = 40;
|
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})…` });
|
(0, job_store_1.updateJob)(job.id, { status: 'running', progress: `Starting ${config.name} (${llm.modelId})…` });
|
||||||
while (turn < MAX_TURNS) {
|
while (turn < MAX_TURNS) {
|
||||||
turn++;
|
turn++;
|
||||||
|
const systemPrompt = (0, loader_1.resolvePrompt)(config.promptId);
|
||||||
const messages = [
|
const messages = [
|
||||||
{ role: 'system', content: config.systemPrompt },
|
{ role: 'system', content: systemPrompt },
|
||||||
...history
|
...history
|
||||||
];
|
];
|
||||||
const response = await llm.chat(messages, oaiTools, 8192);
|
const response = await llm.chat(messages, oaiTools, 8192);
|
||||||
|
|||||||
1
dist/agents/coder.d.ts
vendored
Normal file
1
dist/agents/coder.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export {};
|
||||||
16
dist/agents/coder.js
vendored
Normal file
16
dist/agents/coder.js
vendored
Normal file
@@ -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'
|
||||||
|
])
|
||||||
|
});
|
||||||
9
dist/agents/index.d.ts
vendored
Normal file
9
dist/agents/index.d.ts
vendored
Normal file
@@ -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';
|
||||||
19
dist/agents/index.js
vendored
Normal file
19
dist/agents/index.js
vendored
Normal file
@@ -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; } });
|
||||||
1
dist/agents/marketing.d.ts
vendored
Normal file
1
dist/agents/marketing.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export {};
|
||||||
14
dist/agents/marketing.js
vendored
Normal file
14
dist/agents/marketing.js
vendored
Normal file
@@ -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'
|
||||||
|
])
|
||||||
|
});
|
||||||
1
dist/agents/orchestrator.d.ts
vendored
Normal file
1
dist/agents/orchestrator.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export {};
|
||||||
17
dist/agents/orchestrator.js
vendored
Normal file
17
dist/agents/orchestrator.js
vendored
Normal file
@@ -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'
|
||||||
|
])
|
||||||
|
});
|
||||||
1
dist/agents/pm.d.ts
vendored
Normal file
1
dist/agents/pm.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export {};
|
||||||
15
dist/agents/pm.js
vendored
Normal file
15
dist/agents/pm.js
vendored
Normal file
@@ -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'
|
||||||
|
])
|
||||||
|
});
|
||||||
18
dist/agents/registry.d.ts
vendored
Normal file
18
dist/agents/registry.d.ts
vendored
Normal file
@@ -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<string, AgentConfig>;
|
||||||
|
/** Pick tools from ALL_TOOLS by name. */
|
||||||
|
export declare function pick(names: string[]): ToolDefinition[];
|
||||||
34
dist/agents/registry.js
vendored
Normal file
34
dist/agents/registry.js
vendored
Normal file
@@ -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));
|
||||||
|
}
|
||||||
64
dist/orchestrator.js
vendored
64
dist/orchestrator.js
vendored
@@ -5,6 +5,7 @@ exports.clearSession = clearSession;
|
|||||||
exports.orchestratorChat = orchestratorChat;
|
exports.orchestratorChat = orchestratorChat;
|
||||||
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 MAX_TURNS = 20;
|
const MAX_TURNS = 20;
|
||||||
const sessions = new Map();
|
const sessions = new Map();
|
||||||
function getOrCreateSession(sessionId) {
|
function getOrCreateSession(sessionId) {
|
||||||
@@ -32,59 +33,6 @@ function clearSession(sessionId) {
|
|||||||
sessions.delete(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
|
// Main orchestrator chat — uses GLM-5 (Tier B) by default
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
async function orchestratorChat(sessionId, userMessage, ctx, opts) {
|
async function orchestratorChat(sessionId, userMessage, ctx, opts) {
|
||||||
@@ -102,10 +50,12 @@ async function orchestratorChat(sessionId, userMessage, ctx, opts) {
|
|||||||
let finalReply = '';
|
let finalReply = '';
|
||||||
let finalReasoning = null;
|
let finalReasoning = null;
|
||||||
const toolCallNames = [];
|
const toolCallNames = [];
|
||||||
// Build system prompt — inject project knowledge if provided
|
// Resolve system prompt from template — {{knowledge}} injects project memory
|
||||||
const systemContent = opts?.knowledgeContext
|
const systemContent = (0, loader_1.resolvePrompt)('orchestrator', {
|
||||||
? `${SYSTEM_PROMPT}\n\n## Project Memory (known facts)\n${opts.knowledgeContext}`
|
knowledge: opts?.knowledgeContext
|
||||||
: SYSTEM_PROMPT;
|
? `## 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
|
||||||
const buildMessages = () => [
|
const buildMessages = () => [
|
||||||
{ role: 'system', content: systemContent },
|
{ role: 'system', content: systemContent },
|
||||||
|
|||||||
1
dist/prompts/coder.d.ts
vendored
Normal file
1
dist/prompts/coder.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export {};
|
||||||
31
dist/prompts/coder.js
vendored
Normal file
31
dist/prompts/coder.js
vendored
Normal file
@@ -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());
|
||||||
7
dist/prompts/loader.d.ts
vendored
Normal file
7
dist/prompts/loader.d.ts
vendored
Normal file
@@ -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, string>): string;
|
||||||
|
export declare function hasPrompt(id: string): boolean;
|
||||||
30
dist/prompts/loader.js
vendored
Normal file
30
dist/prompts/loader.js
vendored
Normal file
@@ -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);
|
||||||
|
}
|
||||||
1
dist/prompts/marketing.d.ts
vendored
Normal file
1
dist/prompts/marketing.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export {};
|
||||||
18
dist/prompts/marketing.js
vendored
Normal file
18
dist/prompts/marketing.js
vendored
Normal file
@@ -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());
|
||||||
1
dist/prompts/orchestrator.d.ts
vendored
Normal file
1
dist/prompts/orchestrator.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export {};
|
||||||
62
dist/prompts/orchestrator.js
vendored
Normal file
62
dist/prompts/orchestrator.js
vendored
Normal file
@@ -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());
|
||||||
1
dist/prompts/pm.d.ts
vendored
Normal file
1
dist/prompts/pm.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export {};
|
||||||
20
dist/prompts/pm.js
vendored
Normal file
20
dist/prompts/pm.js
vendored
Normal file
@@ -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());
|
||||||
18
dist/server.js
vendored
18
dist/server.js
vendored
@@ -45,15 +45,8 @@ const child_process_1 = require("child_process");
|
|||||||
const job_store_1 = require("./job-store");
|
const job_store_1 = require("./job-store");
|
||||||
const agent_runner_1 = require("./agent-runner");
|
const agent_runner_1 = require("./agent-runner");
|
||||||
const agents_1 = require("./agents");
|
const agents_1 = require("./agents");
|
||||||
|
const security_1 = require("./tools/security");
|
||||||
const orchestrator_1 = require("./orchestrator");
|
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)();
|
const app = (0, express_1.default)();
|
||||||
app.use((0, cors_1.default)());
|
app.use((0, cors_1.default)());
|
||||||
const startTime = new Date();
|
const startTime = new Date();
|
||||||
@@ -71,7 +64,7 @@ function ensureWorkspace(repo) {
|
|||||||
fs.mkdirSync(dir, { recursive: true });
|
fs.mkdirSync(dir, { recursive: true });
|
||||||
return dir;
|
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. ` +
|
throw new Error(`SECURITY: Repo "${repo}" is a protected Vibn platform repo. ` +
|
||||||
`Agents cannot clone or work in this workspace.`);
|
`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
|
// Orchestrator — persistent chat with full project context
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
app.post('/orchestrator/chat', async (req, res) => {
|
app.post('/orchestrator/chat', async (req, res) => {
|
||||||
const { message, session_id } = req.body;
|
const { message, session_id, history, knowledge_context } = req.body;
|
||||||
if (!message) {
|
if (!message) {
|
||||||
res.status(400).json({ error: '"message" is required' });
|
res.status(400).json({ error: '"message" is required' });
|
||||||
return;
|
return;
|
||||||
@@ -204,7 +197,10 @@ app.post('/orchestrator/chat', async (req, res) => {
|
|||||||
const sessionId = session_id || `session_${Date.now()}`;
|
const sessionId = session_id || `session_${Date.now()}`;
|
||||||
const ctx = buildContext();
|
const ctx = buildContext();
|
||||||
try {
|
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);
|
res.json(result);
|
||||||
}
|
}
|
||||||
catch (err) {
|
catch (err) {
|
||||||
|
|||||||
1
dist/tools/index.d.ts
vendored
1
dist/tools/index.d.ts
vendored
@@ -5,6 +5,7 @@ import './gitea';
|
|||||||
import './coolify';
|
import './coolify';
|
||||||
import './agent';
|
import './agent';
|
||||||
import './memory';
|
import './memory';
|
||||||
|
import './skills';
|
||||||
export { ALL_TOOLS, executeTool, ToolDefinition } from './registry';
|
export { ALL_TOOLS, executeTool, ToolDefinition } from './registry';
|
||||||
export { ToolContext, MemoryUpdate } from './context';
|
export { ToolContext, MemoryUpdate } from './context';
|
||||||
export { PROTECTED_GITEA_REPOS, PROTECTED_COOLIFY_PROJECT, PROTECTED_COOLIFY_APPS, assertGiteaWritable, assertCoolifyDeployable } from './security';
|
export { PROTECTED_GITEA_REPOS, PROTECTED_COOLIFY_PROJECT, PROTECTED_COOLIFY_APPS, assertGiteaWritable, assertCoolifyDeployable } from './security';
|
||||||
|
|||||||
1
dist/tools/index.js
vendored
1
dist/tools/index.js
vendored
@@ -10,6 +10,7 @@ require("./gitea");
|
|||||||
require("./coolify");
|
require("./coolify");
|
||||||
require("./agent");
|
require("./agent");
|
||||||
require("./memory");
|
require("./memory");
|
||||||
|
require("./skills");
|
||||||
// Re-export the public API — identical surface to the old tools.ts
|
// Re-export the public API — identical surface to the old tools.ts
|
||||||
var registry_1 = require("./registry");
|
var registry_1 = require("./registry");
|
||||||
Object.defineProperty(exports, "ALL_TOOLS", { enumerable: true, get: function () { return registry_1.ALL_TOOLS; } });
|
Object.defineProperty(exports, "ALL_TOOLS", { enumerable: true, get: function () { return registry_1.ALL_TOOLS; } });
|
||||||
|
|||||||
1
dist/tools/skills.d.ts
vendored
Normal file
1
dist/tools/skills.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export {};
|
||||||
60
dist/tools/skills.js
vendored
Normal file
60
dist/tools/skills.js
vendored
Normal file
@@ -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/<name>/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 };
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { createLLM, toOAITools, LLMMessage } from './llm';
|
import { createLLM, toOAITools, LLMMessage } from './llm';
|
||||||
import { AgentConfig } from './agents';
|
import { AgentConfig } from './agents';
|
||||||
import { executeTool, ToolContext } from './tools';
|
import { executeTool, ToolContext } from './tools';
|
||||||
|
import { resolvePrompt } from './prompts/loader';
|
||||||
import { Job, updateJob } from './job-store';
|
import { Job, updateJob } from './job-store';
|
||||||
|
|
||||||
const MAX_TURNS = 40;
|
const MAX_TURNS = 40;
|
||||||
@@ -40,8 +41,9 @@ export async function runAgent(
|
|||||||
while (turn < MAX_TURNS) {
|
while (turn < MAX_TURNS) {
|
||||||
turn++;
|
turn++;
|
||||||
|
|
||||||
|
const systemPrompt = resolvePrompt(config.promptId);
|
||||||
const messages: LLMMessage[] = [
|
const messages: LLMMessage[] = [
|
||||||
{ role: 'system', content: config.systemPrompt },
|
{ role: 'system', content: systemPrompt },
|
||||||
...history
|
...history
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
133
src/agents.ts
133
src/agents.ts
@@ -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<string, AgentConfig> = {
|
|
||||||
|
|
||||||
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])
|
|
||||||
}
|
|
||||||
};
|
|
||||||
15
src/agents/coder.ts
Normal file
15
src/agents/coder.ts
Normal file
@@ -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'
|
||||||
|
])
|
||||||
|
});
|
||||||
14
src/agents/index.ts
Normal file
14
src/agents/index.ts
Normal file
@@ -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';
|
||||||
13
src/agents/marketing.ts
Normal file
13
src/agents/marketing.ts
Normal file
@@ -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'
|
||||||
|
])
|
||||||
|
});
|
||||||
16
src/agents/orchestrator.ts
Normal file
16
src/agents/orchestrator.ts
Normal file
@@ -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'
|
||||||
|
])
|
||||||
|
});
|
||||||
14
src/agents/pm.ts
Normal file
14
src/agents/pm.ts
Normal file
@@ -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'
|
||||||
|
])
|
||||||
|
});
|
||||||
41
src/agents/registry.ts
Normal file
41
src/agents/registry.ts
Normal file
@@ -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/<id>.ts)
|
||||||
|
tools: ToolDefinition[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const _registry = new Map<string, AgentConfig>();
|
||||||
|
|
||||||
|
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<string, AgentConfig> = new Proxy({} as Record<string, AgentConfig>, {
|
||||||
|
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));
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { createLLM, toOAITools, LLMMessage } from './llm';
|
import { createLLM, toOAITools, LLMMessage } from './llm';
|
||||||
import { ALL_TOOLS, executeTool, ToolContext, MemoryUpdate } from './tools';
|
import { ALL_TOOLS, executeTool, ToolContext, MemoryUpdate } from './tools';
|
||||||
|
import { resolvePrompt } from './prompts/loader';
|
||||||
|
|
||||||
const MAX_TURNS = 20;
|
const MAX_TURNS = 20;
|
||||||
|
|
||||||
@@ -43,60 +44,8 @@ export function clearSession(sessionId: string) {
|
|||||||
sessions.delete(sessionId);
|
sessions.delete(sessionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// Prompt text lives in src/prompts/orchestrator.ts — imported via agents/index.ts
|
||||||
// Orchestrator system prompt
|
// which is loaded before orchestratorChat() is first called.
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
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.`;
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Chat types
|
// Chat types
|
||||||
@@ -150,10 +99,12 @@ export async function orchestratorChat(
|
|||||||
let finalReasoning: string | null = null;
|
let finalReasoning: string | null = null;
|
||||||
const toolCallNames: string[] = [];
|
const toolCallNames: string[] = [];
|
||||||
|
|
||||||
// Build system prompt — inject project knowledge if provided
|
// Resolve system prompt from template — {{knowledge}} injects project memory
|
||||||
const systemContent = opts?.knowledgeContext
|
const systemContent = resolvePrompt('orchestrator', {
|
||||||
? `${SYSTEM_PROMPT}\n\n## Project Memory (known facts)\n${opts.knowledgeContext}`
|
knowledge: opts?.knowledgeContext
|
||||||
: SYSTEM_PROMPT;
|
? `## 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
|
||||||
const buildMessages = (): LLMMessage[] => [
|
const buildMessages = (): LLMMessage[] => [
|
||||||
|
|||||||
30
src/prompts/coder.ts
Normal file
30
src/prompts/coder.ts
Normal file
@@ -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());
|
||||||
28
src/prompts/loader.ts
Normal file
28
src/prompts/loader.ts
Normal file
@@ -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<string, string>();
|
||||||
|
|
||||||
|
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, string> = {}): 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);
|
||||||
|
}
|
||||||
17
src/prompts/marketing.ts
Normal file
17
src/prompts/marketing.ts
Normal file
@@ -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());
|
||||||
61
src/prompts/orchestrator.ts
Normal file
61
src/prompts/orchestrator.ts
Normal file
@@ -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());
|
||||||
19
src/prompts/pm.ts
Normal file
19
src/prompts/pm.ts
Normal file
@@ -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());
|
||||||
@@ -8,16 +8,9 @@ import { createJob, getJob, listJobs, updateJob } from './job-store';
|
|||||||
import { runAgent } from './agent-runner';
|
import { runAgent } from './agent-runner';
|
||||||
import { AGENTS } from './agents';
|
import { AGENTS } from './agents';
|
||||||
import { ToolContext } from './tools';
|
import { ToolContext } from './tools';
|
||||||
|
import { PROTECTED_GITEA_REPOS } from './tools/security';
|
||||||
import { orchestratorChat, listSessions, clearSession } from './orchestrator';
|
import { orchestratorChat, listSessions, clearSession } from './orchestrator';
|
||||||
|
import { LLMMessage } from './llm';
|
||||||
// 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 = express();
|
const app = express();
|
||||||
app.use(cors());
|
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) => {
|
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; }
|
if (!message) { res.status(400).json({ error: '"message" is required' }); return; }
|
||||||
|
|
||||||
const sessionId = session_id || `session_${Date.now()}`;
|
const sessionId = session_id || `session_${Date.now()}`;
|
||||||
const ctx = buildContext();
|
const ctx = buildContext();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await orchestratorChat(sessionId, message, ctx);
|
const result = await orchestratorChat(sessionId, message, ctx, {
|
||||||
|
preloadedHistory: history,
|
||||||
|
knowledgeContext: knowledge_context
|
||||||
|
});
|
||||||
res.json(result);
|
res.json(result);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import './gitea';
|
|||||||
import './coolify';
|
import './coolify';
|
||||||
import './agent';
|
import './agent';
|
||||||
import './memory';
|
import './memory';
|
||||||
|
import './skills';
|
||||||
|
|
||||||
// Re-export the public API — identical surface to the old tools.ts
|
// Re-export the public API — identical surface to the old tools.ts
|
||||||
export { ALL_TOOLS, executeTool, ToolDefinition } from './registry';
|
export { ALL_TOOLS, executeTool, ToolDefinition } from './registry';
|
||||||
|
|||||||
62
src/tools/skills.ts
Normal file
62
src/tools/skills.ts
Normal file
@@ -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<any> {
|
||||||
|
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/<name>/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 };
|
||||||
|
}
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user