feat: add Atlas PRD agent for product discovery
- src/prompts/atlas.ts — full Atlas system prompt (6-phase PM discovery flow) - src/tools/prd.ts — finalize_prd tool that signals PRD completion - src/agents/atlas.ts — Atlas agent config (Tier A, conversational) - src/atlas.ts — atlasChat() multi-turn session handler - server.ts — /atlas/chat, /atlas/sessions endpoints Made-with: Cursor
This commit is contained in:
1
dist/agents/atlas.d.ts
vendored
Normal file
1
dist/agents/atlas.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export {};
|
||||
10
dist/agents/atlas.js
vendored
Normal file
10
dist/agents/atlas.js
vendored
Normal file
@@ -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'])
|
||||
});
|
||||
2
dist/agents/index.d.ts
vendored
2
dist/agents/index.d.ts
vendored
@@ -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';
|
||||
|
||||
2
dist/agents/index.js
vendored
2
dist/agents/index.js
vendored
@@ -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; } });
|
||||
|
||||
21
dist/atlas.d.ts
vendored
Normal file
21
dist/atlas.d.ts
vendored
Normal file
@@ -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<AtlasChatResult>;
|
||||
113
dist/atlas.js
vendored
Normal file
113
dist/atlas.js
vendored
Normal file
@@ -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
|
||||
};
|
||||
}
|
||||
1
dist/prompts/atlas.d.ts
vendored
Normal file
1
dist/prompts/atlas.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export {};
|
||||
155
dist/prompts/atlas.js
vendored
Normal file
155
dist/prompts/atlas.js
vendored
Normal file
@@ -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());
|
||||
27
dist/server.js
vendored
27
dist/server.js
vendored
@@ -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);
|
||||
|
||||
1
dist/tools/index.d.ts
vendored
1
dist/tools/index.d.ts
vendored
@@ -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';
|
||||
|
||||
1
dist/tools/index.js
vendored
1
dist/tools/index.js
vendored
@@ -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; } });
|
||||
|
||||
2
dist/tools/prd.d.ts
vendored
Normal file
2
dist/tools/prd.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
/** Shared state — atlas.ts reads this after each turn to check if PRD was finalized. */
|
||||
export declare const prdStore: Map<string, string>;
|
||||
29
dist/tools/prd.js
vendored
Normal file
29
dist/tools/prd.js
vendored
Normal file
@@ -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.'
|
||||
};
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user