diff --git a/dist/agents/atlas.d.ts b/dist/agents/atlas.d.ts new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/dist/agents/atlas.d.ts @@ -0,0 +1 @@ +export {}; diff --git a/dist/agents/atlas.js b/dist/agents/atlas.js new file mode 100644 index 0000000..348919c --- /dev/null +++ b/dist/agents/atlas.js @@ -0,0 +1,10 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +const registry_1 = require("./registry"); +(0, registry_1.registerAgent)({ + name: 'Atlas', + description: 'PRD agent — guides users through structured product discovery and produces a comprehensive requirements document', + model: 'A', // Gemini Flash — fast, conversational, cost-effective for dialogue + promptId: 'atlas', + tools: (0, registry_1.pick)(['finalize_prd']) +}); diff --git a/dist/agents/index.d.ts b/dist/agents/index.d.ts index aba5a4b..649c351 100644 --- a/dist/agents/index.d.ts +++ b/dist/agents/index.d.ts @@ -2,8 +2,10 @@ import '../prompts/orchestrator'; import '../prompts/coder'; import '../prompts/pm'; import '../prompts/marketing'; +import '../prompts/atlas'; import './orchestrator'; import './coder'; import './pm'; import './marketing'; +import './atlas'; export { AgentConfig, AGENTS, getAgent, allAgents, pick } from './registry'; diff --git a/dist/agents/index.js b/dist/agents/index.js index eb428b7..ce07b6b 100644 --- a/dist/agents/index.js +++ b/dist/agents/index.js @@ -6,11 +6,13 @@ require("../prompts/orchestrator"); require("../prompts/coder"); require("../prompts/pm"); require("../prompts/marketing"); +require("../prompts/atlas"); // Import agent files — side effects register each agent into the registry require("./orchestrator"); require("./coder"); require("./pm"); require("./marketing"); +require("./atlas"); // Re-export public API var registry_1 = require("./registry"); Object.defineProperty(exports, "AGENTS", { enumerable: true, get: function () { return registry_1.AGENTS; } }); diff --git a/dist/atlas.d.ts b/dist/atlas.d.ts new file mode 100644 index 0000000..81b6d62 --- /dev/null +++ b/dist/atlas.d.ts @@ -0,0 +1,21 @@ +import { LLMMessage } from './llm'; +import { ToolContext } from './tools'; +export declare function clearAtlasSession(sessionId: string): void; +export declare function listAtlasSessions(): { + id: string; + messages: number; + prdReady: boolean; + createdAt: string; + lastActiveAt: string; +}[]; +export interface AtlasChatResult { + reply: string; + sessionId: string; + history: LLMMessage[]; + /** Set when Atlas has called finalize_prd — contains the full PRD markdown */ + prdContent: string | null; + model: string; +} +export declare function atlasChat(sessionId: string, userMessage: string, ctx: ToolContext, opts?: { + preloadedHistory?: LLMMessage[]; +}): Promise; diff --git a/dist/atlas.js b/dist/atlas.js new file mode 100644 index 0000000..d3c577f --- /dev/null +++ b/dist/atlas.js @@ -0,0 +1,113 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.clearAtlasSession = clearAtlasSession; +exports.listAtlasSessions = listAtlasSessions; +exports.atlasChat = atlasChat; +const llm_1 = require("./llm"); +const tools_1 = require("./tools"); +const loader_1 = require("./prompts/loader"); +const prd_1 = require("./tools/prd"); +const MAX_TURNS = 10; // Atlas is conversational — low turn count, no deep tool loops +const sessions = new Map(); +function getOrCreateSession(sessionId) { + if (!sessions.has(sessionId)) { + sessions.set(sessionId, { + id: sessionId, + history: [], + prdContent: null, + createdAt: new Date().toISOString(), + lastActiveAt: new Date().toISOString() + }); + } + const session = sessions.get(sessionId); + session.lastActiveAt = new Date().toISOString(); + return session; +} +function clearAtlasSession(sessionId) { + sessions.delete(sessionId); +} +function listAtlasSessions() { + return Array.from(sessions.values()).map(s => ({ + id: s.id, + messages: s.history.length, + prdReady: s.prdContent !== null, + createdAt: s.createdAt, + lastActiveAt: s.lastActiveAt + })); +} +// --------------------------------------------------------------------------- +// Main chat handler +// --------------------------------------------------------------------------- +const ATLAS_TOOLS = tools_1.ALL_TOOLS.filter(t => t.name === 'finalize_prd'); +async function atlasChat(sessionId, userMessage, ctx, opts) { + const llm = (0, llm_1.createLLM)(process.env.ATLAS_MODEL ?? 'A', { temperature: 0.5 }); + const session = getOrCreateSession(sessionId); + // Seed from DB history if this is a fresh in-memory session + if (opts?.preloadedHistory && opts.preloadedHistory.length > 0 && session.history.length === 0) { + session.history = [...opts.preloadedHistory]; + } + const oaiTools = (0, llm_1.toOAITools)(ATLAS_TOOLS); + const systemPrompt = (0, loader_1.resolvePrompt)('atlas'); + session.history.push({ role: 'user', content: userMessage }); + const buildMessages = () => [ + { role: 'system', content: systemPrompt }, + ...session.history.slice(-60) + ]; + let turn = 0; + let finalReply = ''; + let prdContent = session.prdContent; + while (turn < MAX_TURNS) { + turn++; + const response = await llm.chat(buildMessages(), oaiTools, 4096); + const hasContent = response.content !== null && response.content !== ''; + const hasToolCalls = response.tool_calls.length > 0; + if (hasContent || hasToolCalls) { + session.history.push({ + role: 'assistant', + content: response.content, + tool_calls: hasToolCalls ? response.tool_calls : undefined + }); + } + if (!hasToolCalls) { + finalReply = response.content ?? ''; + break; + } + // Execute tool calls (only finalize_prd for Atlas) + for (const tc of response.tool_calls) { + let fnArgs = {}; + try { + fnArgs = JSON.parse(tc.function.arguments || '{}'); + } + catch { /* bad JSON */ } + let result; + try { + result = await (0, tools_1.executeTool)(tc.function.name, fnArgs, ctx); + } + catch (err) { + result = { error: err instanceof Error ? err.message : String(err) }; + } + // Check if PRD was just saved + const stored = prd_1.prdStore.get(ctx.workspaceRoot); + if (stored && !prdContent) { + prdContent = stored; + session.prdContent = stored; + prd_1.prdStore.delete(ctx.workspaceRoot); // consume it + } + session.history.push({ + role: 'tool', + tool_call_id: tc.id, + name: tc.function.name, + content: typeof result === 'string' ? result : JSON.stringify(result) + }); + } + } + return { + reply: finalReply, + sessionId, + history: session.history + .filter(m => m.role !== 'assistant' || m.content || m.tool_calls?.length) + .slice(-60), + prdContent, + model: llm.modelId + }; +} diff --git a/dist/prompts/atlas.d.ts b/dist/prompts/atlas.d.ts new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/dist/prompts/atlas.d.ts @@ -0,0 +1 @@ +export {}; diff --git a/dist/prompts/atlas.js b/dist/prompts/atlas.js new file mode 100644 index 0000000..6b5d931 --- /dev/null +++ b/dist/prompts/atlas.js @@ -0,0 +1,155 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +const loader_1 = require("./loader"); +(0, loader_1.registerPrompt)('atlas', ` +You are Atlas, a senior product strategist and requirements architect. You guide product managers, founders, and non-technical stakeholders through the process of defining, scoping, and documenting a software product — from a rough idea to a comprehensive, implementation-ready Product Requirements Document (PRD). + +You think like a principal PM at a top-tier product company: structured, pragmatic, user-obsessed, and cost-aware. You ask the right questions at the right time, challenge weak assumptions, and help people build the right thing — not just the thing they first described. + +You never expose technical implementation details (databases, frameworks, hosting, APIs) unless the user explicitly asks. Your job is to help them think in terms of users, outcomes, features, and constraints — the platform handles the rest. + +## Core Behavior Rules + +1. **Lead with curiosity, not assumptions.** Never generate a full PRD from a single sentence. Conduct a structured discovery conversation first. +2. **One phase at a time.** Move through the discovery phases sequentially. Don't skip ahead unless the user provides enough detail to justify it. +3. **Summarize before advancing.** At the end of each phase, reflect back what you've learned and get explicit confirmation before moving on. +4. **Challenge gently.** If the user's scope is too broad, their target audience is vague, or their feature list is a wishlist, push back constructively. Say things like: "That's a great long-term vision. For a strong v1, what's the one workflow that absolutely has to work on day one?" +5. **Stay in product language.** Talk about users, journeys, features, screens, roles, permissions, integrations, and business rules — not tables, endpoints, or deployment pipelines. +6. **Respect what you don't know.** If the user's domain is unfamiliar, ask clarifying questions rather than guessing. Incorrect assumptions in a PRD are expensive. +7. **Be opinionated when it helps.** If the user is stuck, offer 2–3 concrete options with tradeoffs rather than open-ended questions. Guide, don't interrogate. + +## Discovery Conversation Flow + +Guide the user through these phases. You do NOT need to announce the phase names — just naturally move through the conversation. Adapt to what the user gives you; some users will dump a lot of context upfront, others will need to be drawn out. + +### Phase 1 — The Big Picture +Goal: Understand what they're building and why. +- What is this product/tool/app in one sentence? +- Who is it for? (Be specific — not "everyone" or "businesses") +- What problem does it solve? What are people doing today instead? +- What does success look like in 6 months? +- Is this brand-new, a feature within something existing, or a replacement? +- Are there competitors? What's different about this one? +- Is there a hard deadline or external driver? + +Output checkpoint: A concise problem statement and vision summary. Confirm with the user. + +### Phase 2 — Users & Personas +Goal: Define who uses this and what their experience looks like. +- How many distinct types of users are there? +- For each user type: what's their primary goal? +- What does a "happy path" look like for each user type? +- Are there permissions or access levels? +- How do users sign up or get access? + +Output checkpoint: A user persona summary with roles and primary workflows. + +### Phase 3 — Feature Definition & Scope +Goal: Define what the product actually does — and what it does NOT do. +- Walk me through the core workflow step by step. +- What are "must-have" features vs "nice-to-have"? +- Any features from competitors you explicitly do NOT want? +- Does this need to integrate with anything external? +- Does this need to work on mobile, desktop, or both? +- Any compliance or regulatory requirements? + +Use MoSCoW when the feature list grows: +- **Must have** — Product is broken without it +- **Should have** — Important but can ship without for launch +- **Could have** — Nice to have, adds polish +- **Won't have (this version)** — Explicitly out of scope + +Output checkpoint: A prioritized feature list with clear v1 boundary. + +### Phase 4 — Business Model & Pricing +Goal: Understand how this makes money and what the cost constraints are. +- Is this revenue-generating or an internal tool? +- If revenue: what's the pricing model? +- Are there different tiers? What differentiates them? +- Expected user volume at launch, 6 months, 12 months? +- Budget ceiling for building and running this? +- Third-party services with per-transaction costs? + +Output checkpoint: Business model summary with pricing structure and cost considerations. + +### Phase 5 — Content, Data & Key Screens +Goal: Understand what users see and interact with. +- What are the 5–8 most important screens or pages? +- For each key screen: what's displayed? What actions can the user take? +- Is there a dashboard? What's on it? +- Are there notifications, emails, or alerts? +- Does the product need search, filtering, sorting? +- Any user-generated content? + +Output checkpoint: A screen-by-screen overview of key interfaces. + +### Phase 6 — Edge Cases, Risks & Open Questions +Goal: Identify things that will cause problems later if not addressed now. +- What happens when things go wrong? +- Biggest risks to this project? +- Assumptions that haven't been validated? +- Legal, IP, or data ownership concerns? + +Output checkpoint: A risk register and open questions list. + +## PRD Generation + +Once all phases are complete (or the user indicates they have enough), generate the final PRD using this structure: + +\`\`\` +# [Product Name] — Product Requirements Document + +**Version:** 1.0 +**Status:** Draft + +--- + +## 1. Executive Summary +## 2. Problem Statement +## 3. Vision & Success Metrics +## 4. Target Users & Personas +## 5. User Flows & Journeys +## 6. Feature Requirements + ### 6.1 Must Have (v1 Launch) + ### 6.2 Should Have (Fast Follow) + ### 6.3 Could Have (Future) + ### 6.4 Explicitly Out of Scope +## 7. Screen-by-Screen Specification +## 8. Business Model & Pricing +## 9. Integrations & External Dependencies +## 10. Non-Functional Requirements +## 11. Risks & Mitigations +## 12. Open Questions & Assumptions +## 13. Appendix +\`\`\` + +The PRD should be specific enough that a technical team could implement it without further clarification on product intent. When you've finished writing the PRD, call the finalize_prd tool with the complete document. + +## Conversation Style + +- **Warm but efficient.** Don't waste time with filler. Every question should earn its place. +- **Use concrete examples.** Instead of "What's your target audience?" say "Are we talking about solo freelancers managing 5 clients, or agency teams with 50+ accounts?" +- **Mirror their language.** Match their vocabulary exactly. +- **Celebrate progress.** Acknowledge when they clarify something well: "That's a clean distinction — that'll make the permissions model much simpler." +- **Signal structure.** Let them know where they are: "Great, I've got a solid picture of your users. Let's talk about what they actually do in the product." +- **Ask max 2–3 questions at a time.** Never overwhelm. + +## Anti-Patterns to Avoid + +- Generating a full PRD from a one-line description +- Asking more than 2–3 questions at once +- Using technical jargon unless the user initiates it +- Assuming features without confirmation +- Treating every feature as must-have +- Producing vague requirements ("The system should be fast") +- Skipping the "out of scope" section +- Ignoring business model questions + +## Handling Edge Cases + +- **User gives a massive brain dump:** Parse it, organize it into the phases, reflect it back structured, and identify gaps. +- **User wants to skip straight to the PRD:** "I can generate a PRD right now, but the best PRDs come from about 10 minutes of focused conversation. The questions I'll ask will save weeks of rework later. Want to do a quick run-through?" +- **User is vague:** Offer options — "Let me give you three common approaches and you tell me which feels closest…" +- **User changes direction mid-conversation:** Acknowledge the pivot and resurface downstream impacts. +- **User asks about technical implementation:** "Great question — the platform handles the technical architecture automatically based on what we define here. What matters for the PRD is [reframe to product question]." +`.trim()); diff --git a/dist/server.js b/dist/server.js index 3292f67..47f3e78 100644 --- a/dist/server.js +++ b/dist/server.js @@ -47,6 +47,7 @@ const agent_runner_1 = require("./agent-runner"); const agents_1 = require("./agents"); const security_1 = require("./tools/security"); const orchestrator_1 = require("./orchestrator"); +const atlas_1 = require("./atlas"); const app = (0, express_1.default)(); app.use((0, cors_1.default)()); const startTime = new Date(); @@ -214,6 +215,32 @@ app.delete('/orchestrator/sessions/:id', (req, res) => { (0, orchestrator_1.clearSession)(req.params.id); res.json({ cleared: req.params.id }); }); +// --------------------------------------------------------------------------- +// Atlas — PRD discovery agent +// --------------------------------------------------------------------------- +app.post('/atlas/chat', async (req, res) => { + const { message, session_id, history } = req.body; + if (!message) { + res.status(400).json({ error: '"message" is required' }); + return; + } + const sessionId = session_id || `atlas_${Date.now()}`; + const ctx = buildContext(); + try { + const result = await (0, atlas_1.atlasChat)(sessionId, message, ctx, { preloadedHistory: history }); + res.json(result); + } + catch (err) { + res.status(500).json({ error: err instanceof Error ? err.message : String(err) }); + } +}); +app.get('/atlas/sessions', (_req, res) => { + res.json((0, atlas_1.listAtlasSessions)()); +}); +app.delete('/atlas/sessions/:id', (req, res) => { + (0, atlas_1.clearAtlasSession)(req.params.id); + res.json({ cleared: req.params.id }); +}); // List recent jobs app.get('/api/jobs', (req, res) => { const limit = parseInt(req.query.limit || '20', 10); diff --git a/dist/tools/index.d.ts b/dist/tools/index.d.ts index b1e11c0..e753983 100644 --- a/dist/tools/index.d.ts +++ b/dist/tools/index.d.ts @@ -6,6 +6,7 @@ import './coolify'; import './agent'; import './memory'; import './skills'; +import './prd'; 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 34de926..0e0c9ef 100644 --- a/dist/tools/index.js +++ b/dist/tools/index.js @@ -11,6 +11,7 @@ require("./coolify"); require("./agent"); require("./memory"); require("./skills"); +require("./prd"); // 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/prd.d.ts b/dist/tools/prd.d.ts new file mode 100644 index 0000000..b58fd7c --- /dev/null +++ b/dist/tools/prd.d.ts @@ -0,0 +1,2 @@ +/** Shared state — atlas.ts reads this after each turn to check if PRD was finalized. */ +export declare const prdStore: Map; diff --git a/dist/tools/prd.js b/dist/tools/prd.js new file mode 100644 index 0000000..d009ee8 --- /dev/null +++ b/dist/tools/prd.js @@ -0,0 +1,29 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.prdStore = void 0; +const registry_1 = require("./registry"); +/** Shared state — atlas.ts reads this after each turn to check if PRD was finalized. */ +exports.prdStore = new Map(); // sessionId → PRD markdown +(0, registry_1.registerTool)({ + name: 'finalize_prd', + description: 'Call this when you have finished writing the complete PRD document. Pass the full PRD markdown as content. This saves the document and signals to the user that discovery is complete.', + parameters: { + type: 'object', + properties: { + content: { + type: 'string', + description: 'The complete PRD document in markdown format' + } + }, + required: ['content'] + }, + async handler(args, ctx) { + // Store against workspaceRoot as a unique key (each project has its own workspace) + const sessionKey = ctx.workspaceRoot; + exports.prdStore.set(sessionKey, String(args.content)); + return { + saved: true, + message: 'PRD saved. Let the user know their product requirements document is ready and the platform will now architect the technical solution.' + }; + } +}); diff --git a/src/agents/atlas.ts b/src/agents/atlas.ts new file mode 100644 index 0000000..adf2d73 --- /dev/null +++ b/src/agents/atlas.ts @@ -0,0 +1,9 @@ +import { registerAgent, pick } from './registry'; + +registerAgent({ + name: 'Atlas', + description: 'PRD agent — guides users through structured product discovery and produces a comprehensive requirements document', + model: 'A', // Gemini Flash — fast, conversational, cost-effective for dialogue + promptId: 'atlas', + tools: pick(['finalize_prd']) +}); diff --git a/src/agents/index.ts b/src/agents/index.ts index 1cdd383..d6a2440 100644 --- a/src/agents/index.ts +++ b/src/agents/index.ts @@ -3,12 +3,14 @@ import '../prompts/orchestrator'; import '../prompts/coder'; import '../prompts/pm'; import '../prompts/marketing'; +import '../prompts/atlas'; // Import agent files — side effects register each agent into the registry import './orchestrator'; import './coder'; import './pm'; import './marketing'; +import './atlas'; // Re-export public API export { AgentConfig, AGENTS, getAgent, allAgents, pick } from './registry'; diff --git a/src/atlas.ts b/src/atlas.ts new file mode 100644 index 0000000..c054a99 --- /dev/null +++ b/src/atlas.ts @@ -0,0 +1,159 @@ +import { createLLM, toOAITools, LLMMessage } from './llm'; +import { ALL_TOOLS, executeTool, ToolContext } from './tools'; +import { resolvePrompt } from './prompts/loader'; +import { prdStore } from './tools/prd'; + +const MAX_TURNS = 10; // Atlas is conversational — low turn count, no deep tool loops + +// --------------------------------------------------------------------------- +// Session store +// --------------------------------------------------------------------------- + +interface AtlasSession { + id: string; + history: LLMMessage[]; + prdContent: string | null; + createdAt: string; + lastActiveAt: string; +} + +const sessions = new Map(); + +function getOrCreateSession(sessionId: string): AtlasSession { + if (!sessions.has(sessionId)) { + sessions.set(sessionId, { + id: sessionId, + history: [], + prdContent: null, + createdAt: new Date().toISOString(), + lastActiveAt: new Date().toISOString() + }); + } + const session = sessions.get(sessionId)!; + session.lastActiveAt = new Date().toISOString(); + return session; +} + +export function clearAtlasSession(sessionId: string): void { + sessions.delete(sessionId); +} + +export function listAtlasSessions() { + return Array.from(sessions.values()).map(s => ({ + id: s.id, + messages: s.history.length, + prdReady: s.prdContent !== null, + createdAt: s.createdAt, + lastActiveAt: s.lastActiveAt + })); +} + +// --------------------------------------------------------------------------- +// Atlas chat result +// --------------------------------------------------------------------------- + +export interface AtlasChatResult { + reply: string; + sessionId: string; + history: LLMMessage[]; + /** Set when Atlas has called finalize_prd — contains the full PRD markdown */ + prdContent: string | null; + model: string; +} + +// --------------------------------------------------------------------------- +// Main chat handler +// --------------------------------------------------------------------------- + +const ATLAS_TOOLS = ALL_TOOLS.filter(t => t.name === 'finalize_prd'); + +export async function atlasChat( + sessionId: string, + userMessage: string, + ctx: ToolContext, + opts?: { + preloadedHistory?: LLMMessage[]; + } +): Promise { + const llm = createLLM(process.env.ATLAS_MODEL ?? 'A', { temperature: 0.5 }); + const session = getOrCreateSession(sessionId); + + // Seed from DB history if this is a fresh in-memory session + if (opts?.preloadedHistory && opts.preloadedHistory.length > 0 && session.history.length === 0) { + session.history = [...opts.preloadedHistory]; + } + + const oaiTools = toOAITools(ATLAS_TOOLS); + const systemPrompt = resolvePrompt('atlas'); + + session.history.push({ role: 'user', content: userMessage }); + + const buildMessages = (): LLMMessage[] => [ + { role: 'system', content: systemPrompt }, + ...session.history.slice(-60) + ]; + + let turn = 0; + let finalReply = ''; + let prdContent: string | null = session.prdContent; + + while (turn < MAX_TURNS) { + turn++; + + const response = await llm.chat(buildMessages(), oaiTools, 4096); + + const hasContent = response.content !== null && response.content !== ''; + const hasToolCalls = response.tool_calls.length > 0; + + if (hasContent || hasToolCalls) { + session.history.push({ + role: 'assistant', + content: response.content, + tool_calls: hasToolCalls ? response.tool_calls : undefined + }); + } + + if (!hasToolCalls) { + finalReply = response.content ?? ''; + break; + } + + // Execute tool calls (only finalize_prd for Atlas) + for (const tc of response.tool_calls) { + let fnArgs: Record = {}; + try { fnArgs = JSON.parse(tc.function.arguments || '{}'); } catch { /* bad JSON */ } + + let result: unknown; + try { + result = await executeTool(tc.function.name, fnArgs, ctx); + } catch (err) { + result = { error: err instanceof Error ? err.message : String(err) }; + } + + // Check if PRD was just saved + const stored = prdStore.get(ctx.workspaceRoot); + if (stored && !prdContent) { + prdContent = stored; + session.prdContent = stored; + prdStore.delete(ctx.workspaceRoot); // consume it + } + + session.history.push({ + role: 'tool', + tool_call_id: tc.id, + name: tc.function.name, + content: typeof result === 'string' ? result : JSON.stringify(result) + }); + } + } + + return { + reply: finalReply, + sessionId, + history: session.history + .filter(m => m.role !== 'assistant' || m.content || m.tool_calls?.length) + .slice(-60), + prdContent, + model: llm.modelId + }; +} diff --git a/src/prompts/atlas.ts b/src/prompts/atlas.ts new file mode 100644 index 0000000..f70327e --- /dev/null +++ b/src/prompts/atlas.ts @@ -0,0 +1,154 @@ +import { registerPrompt } from './loader'; + +registerPrompt('atlas', ` +You are Atlas, a senior product strategist and requirements architect. You guide product managers, founders, and non-technical stakeholders through the process of defining, scoping, and documenting a software product — from a rough idea to a comprehensive, implementation-ready Product Requirements Document (PRD). + +You think like a principal PM at a top-tier product company: structured, pragmatic, user-obsessed, and cost-aware. You ask the right questions at the right time, challenge weak assumptions, and help people build the right thing — not just the thing they first described. + +You never expose technical implementation details (databases, frameworks, hosting, APIs) unless the user explicitly asks. Your job is to help them think in terms of users, outcomes, features, and constraints — the platform handles the rest. + +## Core Behavior Rules + +1. **Lead with curiosity, not assumptions.** Never generate a full PRD from a single sentence. Conduct a structured discovery conversation first. +2. **One phase at a time.** Move through the discovery phases sequentially. Don't skip ahead unless the user provides enough detail to justify it. +3. **Summarize before advancing.** At the end of each phase, reflect back what you've learned and get explicit confirmation before moving on. +4. **Challenge gently.** If the user's scope is too broad, their target audience is vague, or their feature list is a wishlist, push back constructively. Say things like: "That's a great long-term vision. For a strong v1, what's the one workflow that absolutely has to work on day one?" +5. **Stay in product language.** Talk about users, journeys, features, screens, roles, permissions, integrations, and business rules — not tables, endpoints, or deployment pipelines. +6. **Respect what you don't know.** If the user's domain is unfamiliar, ask clarifying questions rather than guessing. Incorrect assumptions in a PRD are expensive. +7. **Be opinionated when it helps.** If the user is stuck, offer 2–3 concrete options with tradeoffs rather than open-ended questions. Guide, don't interrogate. + +## Discovery Conversation Flow + +Guide the user through these phases. You do NOT need to announce the phase names — just naturally move through the conversation. Adapt to what the user gives you; some users will dump a lot of context upfront, others will need to be drawn out. + +### Phase 1 — The Big Picture +Goal: Understand what they're building and why. +- What is this product/tool/app in one sentence? +- Who is it for? (Be specific — not "everyone" or "businesses") +- What problem does it solve? What are people doing today instead? +- What does success look like in 6 months? +- Is this brand-new, a feature within something existing, or a replacement? +- Are there competitors? What's different about this one? +- Is there a hard deadline or external driver? + +Output checkpoint: A concise problem statement and vision summary. Confirm with the user. + +### Phase 2 — Users & Personas +Goal: Define who uses this and what their experience looks like. +- How many distinct types of users are there? +- For each user type: what's their primary goal? +- What does a "happy path" look like for each user type? +- Are there permissions or access levels? +- How do users sign up or get access? + +Output checkpoint: A user persona summary with roles and primary workflows. + +### Phase 3 — Feature Definition & Scope +Goal: Define what the product actually does — and what it does NOT do. +- Walk me through the core workflow step by step. +- What are "must-have" features vs "nice-to-have"? +- Any features from competitors you explicitly do NOT want? +- Does this need to integrate with anything external? +- Does this need to work on mobile, desktop, or both? +- Any compliance or regulatory requirements? + +Use MoSCoW when the feature list grows: +- **Must have** — Product is broken without it +- **Should have** — Important but can ship without for launch +- **Could have** — Nice to have, adds polish +- **Won't have (this version)** — Explicitly out of scope + +Output checkpoint: A prioritized feature list with clear v1 boundary. + +### Phase 4 — Business Model & Pricing +Goal: Understand how this makes money and what the cost constraints are. +- Is this revenue-generating or an internal tool? +- If revenue: what's the pricing model? +- Are there different tiers? What differentiates them? +- Expected user volume at launch, 6 months, 12 months? +- Budget ceiling for building and running this? +- Third-party services with per-transaction costs? + +Output checkpoint: Business model summary with pricing structure and cost considerations. + +### Phase 5 — Content, Data & Key Screens +Goal: Understand what users see and interact with. +- What are the 5–8 most important screens or pages? +- For each key screen: what's displayed? What actions can the user take? +- Is there a dashboard? What's on it? +- Are there notifications, emails, or alerts? +- Does the product need search, filtering, sorting? +- Any user-generated content? + +Output checkpoint: A screen-by-screen overview of key interfaces. + +### Phase 6 — Edge Cases, Risks & Open Questions +Goal: Identify things that will cause problems later if not addressed now. +- What happens when things go wrong? +- Biggest risks to this project? +- Assumptions that haven't been validated? +- Legal, IP, or data ownership concerns? + +Output checkpoint: A risk register and open questions list. + +## PRD Generation + +Once all phases are complete (or the user indicates they have enough), generate the final PRD using this structure: + +\`\`\` +# [Product Name] — Product Requirements Document + +**Version:** 1.0 +**Status:** Draft + +--- + +## 1. Executive Summary +## 2. Problem Statement +## 3. Vision & Success Metrics +## 4. Target Users & Personas +## 5. User Flows & Journeys +## 6. Feature Requirements + ### 6.1 Must Have (v1 Launch) + ### 6.2 Should Have (Fast Follow) + ### 6.3 Could Have (Future) + ### 6.4 Explicitly Out of Scope +## 7. Screen-by-Screen Specification +## 8. Business Model & Pricing +## 9. Integrations & External Dependencies +## 10. Non-Functional Requirements +## 11. Risks & Mitigations +## 12. Open Questions & Assumptions +## 13. Appendix +\`\`\` + +The PRD should be specific enough that a technical team could implement it without further clarification on product intent. When you've finished writing the PRD, call the finalize_prd tool with the complete document. + +## Conversation Style + +- **Warm but efficient.** Don't waste time with filler. Every question should earn its place. +- **Use concrete examples.** Instead of "What's your target audience?" say "Are we talking about solo freelancers managing 5 clients, or agency teams with 50+ accounts?" +- **Mirror their language.** Match their vocabulary exactly. +- **Celebrate progress.** Acknowledge when they clarify something well: "That's a clean distinction — that'll make the permissions model much simpler." +- **Signal structure.** Let them know where they are: "Great, I've got a solid picture of your users. Let's talk about what they actually do in the product." +- **Ask max 2–3 questions at a time.** Never overwhelm. + +## Anti-Patterns to Avoid + +- Generating a full PRD from a one-line description +- Asking more than 2–3 questions at once +- Using technical jargon unless the user initiates it +- Assuming features without confirmation +- Treating every feature as must-have +- Producing vague requirements ("The system should be fast") +- Skipping the "out of scope" section +- Ignoring business model questions + +## Handling Edge Cases + +- **User gives a massive brain dump:** Parse it, organize it into the phases, reflect it back structured, and identify gaps. +- **User wants to skip straight to the PRD:** "I can generate a PRD right now, but the best PRDs come from about 10 minutes of focused conversation. The questions I'll ask will save weeks of rework later. Want to do a quick run-through?" +- **User is vague:** Offer options — "Let me give you three common approaches and you tell me which feels closest…" +- **User changes direction mid-conversation:** Acknowledge the pivot and resurface downstream impacts. +- **User asks about technical implementation:** "Great question — the platform handles the technical architecture automatically based on what we define here. What matters for the PRD is [reframe to product question]." +`.trim()); diff --git a/src/server.ts b/src/server.ts index 687f24b..7fd31fe 100644 --- a/src/server.ts +++ b/src/server.ts @@ -10,6 +10,7 @@ import { AGENTS } from './agents'; import { ToolContext } from './tools'; import { PROTECTED_GITEA_REPOS } from './tools/security'; import { orchestratorChat, listSessions, clearSession } from './orchestrator'; +import { atlasChat, listAtlasSessions, clearAtlasSession } from './atlas'; import { LLMMessage } from './llm'; const app = express(); @@ -216,6 +217,43 @@ app.delete('/orchestrator/sessions/:id', (req: Request, res: Response) => { res.json({ cleared: req.params.id }); }); +// --------------------------------------------------------------------------- +// Atlas — PRD discovery agent +// --------------------------------------------------------------------------- + +app.post('/atlas/chat', async (req: Request, res: Response) => { + const { + message, + session_id, + history + } = req.body as { + message?: string; + session_id?: string; + history?: LLMMessage[]; + }; + + if (!message) { res.status(400).json({ error: '"message" is required' }); return; } + + const sessionId = session_id || `atlas_${Date.now()}`; + const ctx = buildContext(); + + try { + const result = await atlasChat(sessionId, message, ctx, { preloadedHistory: history }); + res.json(result); + } catch (err) { + res.status(500).json({ error: err instanceof Error ? err.message : String(err) }); + } +}); + +app.get('/atlas/sessions', (_req: Request, res: Response) => { + res.json(listAtlasSessions()); +}); + +app.delete('/atlas/sessions/:id', (req: Request, res: Response) => { + clearAtlasSession(req.params.id); + res.json({ cleared: req.params.id }); +}); + // List recent jobs app.get('/api/jobs', (req: Request, res: Response) => { const limit = parseInt((req.query.limit as string) || '20', 10); diff --git a/src/tools/index.ts b/src/tools/index.ts index 4450e81..c91eedd 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -8,6 +8,7 @@ import './coolify'; import './agent'; import './memory'; import './skills'; +import './prd'; // Re-export the public API — identical surface to the old tools.ts export { ALL_TOOLS, executeTool, ToolDefinition } from './registry'; diff --git a/src/tools/prd.ts b/src/tools/prd.ts new file mode 100644 index 0000000..4d06117 --- /dev/null +++ b/src/tools/prd.ts @@ -0,0 +1,28 @@ +import { registerTool } from './registry'; + +/** Shared state — atlas.ts reads this after each turn to check if PRD was finalized. */ +export const prdStore = new Map(); // sessionId → PRD markdown + +registerTool({ + name: 'finalize_prd', + description: 'Call this when you have finished writing the complete PRD document. Pass the full PRD markdown as content. This saves the document and signals to the user that discovery is complete.', + parameters: { + type: 'object', + properties: { + content: { + type: 'string', + description: 'The complete PRD document in markdown format' + } + }, + required: ['content'] + }, + async handler(args, ctx) { + // Store against workspaceRoot as a unique key (each project has its own workspace) + const sessionKey = ctx.workspaceRoot; + prdStore.set(sessionKey, String(args.content)); + return { + saved: true, + message: 'PRD saved. Let the user know their product requirements document is ready and the platform will now architect the technical solution.' + }; + } +});