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:
2026-03-01 15:38:42 -08:00
parent e91e5e0e37
commit e29dccf745
46 changed files with 759 additions and 272 deletions

1
dist/agents/coder.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
export {};

16
dist/agents/coder.js vendored Normal file
View 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
View 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
View 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
View File

@@ -0,0 +1 @@
export {};

14
dist/agents/marketing.js vendored Normal file
View 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
View File

@@ -0,0 +1 @@
export {};

17
dist/agents/orchestrator.js vendored Normal file
View 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
View File

@@ -0,0 +1 @@
export {};

15
dist/agents/pm.js vendored Normal file
View 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
View 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
View 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));
}