Skip to content

Commit bc5859f

Browse files
committed
Feat: Create 'PM Agent' MVP (Epic -> Ticket Breakdown) #7916
1 parent ab281fa commit bc5859f

1 file changed

Lines changed: 201 additions & 0 deletions

File tree

ai/agents/pm.mjs

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
#!/usr/bin/env node
2+
3+
/**
4+
* Neo.mjs PM Agent (MVP)
5+
*
6+
* This script acts as a "Headless Project Manager Agent".
7+
* It reads a GitHub Epic, breaks it down into technical tasks (using the Protocol),
8+
* and creates the corresponding child issues.
9+
*
10+
* Usage:
11+
* node ai/agents/pm.mjs --epic <issue_number>
12+
*/
13+
14+
import { Command } from 'commander';
15+
import yaml from 'js-yaml';
16+
import dotenv from 'dotenv';
17+
import path from 'path';
18+
import { fileURLToPath } from 'url';
19+
import {
20+
GH_IssueService,
21+
GH_HealthService,
22+
GH_LocalFileService,
23+
KB_QueryService,
24+
KB_DatabaseService
25+
} from '../services.mjs';
26+
27+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
28+
dotenv.config({ path: path.resolve(__dirname, '../../.env') });
29+
30+
const program = new Command();
31+
32+
program
33+
.name('pm-agent')
34+
.description('Autonomous PM Agent for breaking down Epics into Tickets')
35+
.requiredOption('-e, --epic <number>', 'Epic Issue Number to process')
36+
.option('-d, --dry-run', 'Simulate execution without creating issues')
37+
.parse(process.argv);
38+
39+
const options = program.opts();
40+
41+
/**
42+
* Simulates the LLM "Reasoning" step.
43+
*/
44+
async function generateBreakdown(epic, contextDocs) {
45+
console.log('🤖 Thinking... (Simulating LLM Breakdown)');
46+
47+
const contextRefs = contextDocs.map(doc => doc.source).filter(Boolean);
48+
49+
const task1 = {
50+
version: 1.0,
51+
type: 'implementation',
52+
role: 'dev',
53+
goal: `Implement the core logic for "${epic.title}"`,
54+
context: {
55+
epic_issue: epic.number,
56+
files: ["src/Main.mjs"],
57+
knowledge_base_refs: contextRefs.length > 0 ? contextRefs : ["N/A"]
58+
},
59+
requirements: [
60+
"Must follow Neo.mjs class config standards",
61+
"Must include unit tests"
62+
]
63+
};
64+
65+
const task2 = {
66+
version: 1.0,
67+
type: 'implementation',
68+
role: 'dev',
69+
goal: `Update documentation for "${epic.title}"`,
70+
context: {
71+
epic_issue: epic.number
72+
},
73+
requirements: [
74+
"Update Markdown guides",
75+
"Add JSDoc to new classes"
76+
]
77+
};
78+
79+
return [
80+
{ title: `Task: Implement Core Logic for "${epic.title}"`, body: yaml.dump(task1) },
81+
{ title: `Task: Update Documentation for "${epic.title}"`, body: yaml.dump(task2) }
82+
];
83+
}
84+
85+
/**
86+
* Simple frontmatter parser to extract title and body from markdown.
87+
*/
88+
function parseIssueContent(rawContent) {
89+
const parts = rawContent.split('---');
90+
if (parts.length < 3) {
91+
// Fallback if no frontmatter
92+
return { title: 'Unknown Title', body: rawContent };
93+
}
94+
95+
const frontmatter = parts[1];
96+
const body = parts.slice(2).join('---').trim();
97+
98+
let title = 'Unknown Title';
99+
const titleMatch = frontmatter.match(/^title:\s*(.*)$/m);
100+
if (titleMatch) {
101+
title = titleMatch[1].trim().replace(/^['"](.*)['"]$/, '$1'); // Remove quotes if present
102+
}
103+
104+
return { title, body };
105+
}
106+
107+
async function run() {
108+
try {
109+
console.log('🚀 PM Agent Starting...');
110+
111+
// 1. Initialize Services
112+
/*
113+
const ghHealth = await GH_HealthService.healthcheck({});
114+
if (!ghHealth.authenticated) {
115+
throw new Error('GitHub Authentication failed. Check GH_TOKEN.');
116+
}
117+
*/
118+
119+
// Ensure Database is ready for querying
120+
console.log('🔌 Connecting to Knowledge Base...');
121+
await KB_DatabaseService.ready();
122+
123+
// 2. Fetch Epic (Local First)
124+
const epicId = options.epic; // Keep as string for getIssueById
125+
console.log(`📥 Fetching Epic #${epicId} (Local)...`);
126+
127+
// Calling raw method because SDK mapping failed (getIssueById vs get_local_issue_by_id)
128+
const result = await GH_LocalFileService.getIssueById(String(epicId));
129+
130+
if (result.error) {
131+
throw new Error(`Failed to fetch Epic: ${result.message}`);
132+
}
133+
134+
const { title, body } = parseIssueContent(result.content);
135+
const epic = { number: parseInt(epicId), title, body };
136+
137+
console.log(` -> Found: "${epic.title}"`);
138+
139+
// 3. Gather Context
140+
console.log('📚 Querying Knowledge Base for context...');
141+
const context = await KB_QueryService.queryDocuments({
142+
query: epic.title,
143+
limit: 2
144+
});
145+
146+
// 4. Generate Breakdown
147+
const tasks = await generateBreakdown(epic, context.results || []);
148+
149+
// 5. Create Tickets
150+
const createdIds = [];
151+
for (const task of tasks) {
152+
console.log(`✨ Creating Ticket: ${task.title}`);
153+
154+
let issue;
155+
if (options.dryRun) {
156+
console.log(`[Dry Run] Would create ticket with body:\n${task.body}`);
157+
issue = { issueNumber: 0 }; // Mock ID
158+
} else {
159+
issue = await GH_IssueService.createIssue({
160+
title: task.title,
161+
body: task.body, // YAML string
162+
labels: ['agent-task:pending', 'agent-role:dev', 'ai-generated']
163+
});
164+
}
165+
166+
if (issue.error) {
167+
console.error('❌ Failed to create ticket:', issue);
168+
} else {
169+
console.log(` -> Created #${issue.issueNumber}`);
170+
createdIds.push(issue.issueNumber);
171+
}
172+
}
173+
174+
// 6. Link back to Epic
175+
if (createdIds.length > 0) {
176+
console.log('🔗 Linking tickets to Epic...');
177+
const summary = `🤖 **PM Agent Report**\n\nI have broken this Epic down into the following tasks:\n\n` +
178+
createdIds.map(id => `- #${id}`).join('\n');
179+
180+
if (options.dryRun) {
181+
console.log(`[Dry Run] Would post comment to Epic #${epicId}:\n${summary}`);
182+
} else {
183+
await GH_IssueService.createComment({
184+
issue_number: parseInt(epicId),
185+
body: summary,
186+
agent: 'Neo PM Agent'
187+
});
188+
}
189+
}
190+
191+
console.log('✅ PM Agent finished successfully.');
192+
process.exit(0);
193+
194+
} catch (error) {
195+
console.error('❌ PM Agent Failed:', error);
196+
process.exit(1);
197+
}
198+
}
199+
200+
run();
201+

0 commit comments

Comments
 (0)