opencode-athena 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1343 @@
1
+ import { homedir, platform } from 'os';
2
+ import { tool } from '@opencode-ai/plugin';
3
+ import { join, dirname } from 'path';
4
+ import { existsSync } from 'fs';
5
+ import { readFile, mkdir, writeFile, rm } from 'fs/promises';
6
+ import { parse, stringify } from 'yaml';
7
+ import { z } from 'zod';
8
+
9
+ // src/plugin/utils/notifications.ts
10
+ async function sendNotification(message, title, $) {
11
+ const os = platform();
12
+ try {
13
+ if (os === "darwin") {
14
+ await $`osascript -e ${`display notification "${escapeAppleScript(message)}" with title "${escapeAppleScript(title)}"`}`;
15
+ } else if (os === "linux") {
16
+ await $`notify-send ${title} ${message}`;
17
+ } else {
18
+ logNotification(title, message);
19
+ }
20
+ } catch {
21
+ logNotification(title, message);
22
+ }
23
+ }
24
+ function escapeAppleScript(str) {
25
+ return str.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
26
+ }
27
+ function logNotification(title, message) {
28
+ console.log(`[${title}] ${message}`);
29
+ }
30
+
31
+ // src/plugin/hooks/session-hooks.ts
32
+ function createSessionHooks(ctx, tracker, config) {
33
+ return async ({ event }) => {
34
+ const eventType = event.type;
35
+ switch (eventType) {
36
+ case "session.idle":
37
+ await handleSessionIdle(ctx, tracker, config);
38
+ break;
39
+ case "session.created":
40
+ handleSessionCreated(tracker);
41
+ break;
42
+ case "session.error":
43
+ handleSessionError(event, tracker);
44
+ break;
45
+ }
46
+ };
47
+ }
48
+ async function handleSessionIdle(ctx, tracker, config) {
49
+ const currentStory = tracker.getCurrentStory();
50
+ if (currentStory && currentStory.status === "in_progress") {
51
+ if (config.features?.notifications) {
52
+ await sendNotification(
53
+ `Story ${currentStory.id} in progress. Remember to update status when complete!`,
54
+ "OpenCode Athena",
55
+ ctx.$
56
+ );
57
+ }
58
+ }
59
+ }
60
+ function handleSessionCreated(tracker) {
61
+ const currentStory = tracker.getCurrentStory();
62
+ if (currentStory) {
63
+ console.log(`[Athena] Resuming with Story ${currentStory.id} (${currentStory.status})`);
64
+ }
65
+ }
66
+ function handleSessionError(event, tracker) {
67
+ const currentStory = tracker.getCurrentStory();
68
+ if (currentStory && event.error) {
69
+ console.error(`[Athena] Error during Story ${currentStory.id}:`, event.error);
70
+ }
71
+ }
72
+
73
+ // src/plugin/hooks/tool-hooks.ts
74
+ function createToolHooks(_tracker, _config) {
75
+ return {
76
+ /**
77
+ * Called before a tool executes
78
+ * Can modify args or throw to block execution
79
+ */
80
+ before: async (_input, _output) => {
81
+ },
82
+ /**
83
+ * Called after a tool executes
84
+ * Can modify output or append messages
85
+ */
86
+ after: async (_input, _output) => {
87
+ }
88
+ };
89
+ }
90
+
91
+ // src/plugin/hooks/compaction-hook.ts
92
+ function createCompactionHook(tracker) {
93
+ return async (_input, output) => {
94
+ const storyContext = await tracker.getCurrentStoryContext();
95
+ if (storyContext) {
96
+ output.context.push(`## OpenCode Athena - Current BMAD Story Context
97
+
98
+ ${storyContext}
99
+
100
+ IMPORTANT: You are implementing a BMAD story. Use athena_get_story to reload full context if needed. Use athena_update_status to update the story status when complete.
101
+ `);
102
+ }
103
+ };
104
+ }
105
+ var VERSION = "0.0.1";
106
+ var CONFIG_PATHS = {
107
+ /** Global OpenCode config directory */
108
+ globalConfigDir: join(homedir(), ".config", "opencode"),
109
+ /** Global Athena config file */
110
+ globalAthenaConfig: join(homedir(), ".config", "opencode", "athena.json"),
111
+ /** Global OpenCode config file */
112
+ globalOpencodeConfig: join(homedir(), ".config", "opencode", "opencode.json"),
113
+ /** Global oh-my-opencode config file */
114
+ globalOmoConfig: join(homedir(), ".config", "opencode", "oh-my-opencode.json"),
115
+ /** Commands directory */
116
+ commandsDir: join(homedir(), ".config", "opencode", "command"),
117
+ /** Plugin directory */
118
+ pluginDir: join(homedir(), ".config", "opencode", "plugin"),
119
+ /** Athena state file (for story tracking) */
120
+ stateFile: join(homedir(), ".config", "opencode", "athena-state.json")
121
+ };
122
+ var DEFAULTS = {
123
+ /** Default BMAD track for new projects */
124
+ defaultTrack: "bmad-method",
125
+ /** Whether to auto-update sprint status */
126
+ autoStatusUpdate: true,
127
+ /** Maximum parallel stories */
128
+ parallelStoryLimit: 3,
129
+ /** Default features enabled */
130
+ features: {
131
+ bmadBridge: true,
132
+ autoStatus: true,
133
+ parallelExecution: true,
134
+ notifications: true,
135
+ contextMonitor: true,
136
+ commentChecker: true,
137
+ lspTools: true
138
+ },
139
+ /** Default MCPs enabled */
140
+ mcps: {
141
+ context7: true,
142
+ exa: true,
143
+ grepApp: true
144
+ }
145
+ };
146
+
147
+ // src/plugin/tools/config.ts
148
+ function createConfigTool(config) {
149
+ return tool({
150
+ description: `View the current OpenCode Athena configuration.
151
+
152
+ Shows:
153
+ - Version information
154
+ - Subscription settings (Claude, OpenAI, Google)
155
+ - Model assignments for each agent role
156
+ - BMAD integration settings
157
+ - Enabled features and MCP servers`,
158
+ args: {
159
+ section: tool.schema.enum(["all", "subscriptions", "models", "bmad", "features", "mcps"]).optional().describe("Specific section to view (default: all)")
160
+ },
161
+ async execute(args) {
162
+ const section = args.section || "all";
163
+ if (section === "all") {
164
+ return JSON.stringify(
165
+ {
166
+ athenaVersion: VERSION,
167
+ configVersion: config.version,
168
+ subscriptions: config.subscriptions,
169
+ models: config.models,
170
+ bmad: config.bmad,
171
+ features: config.features,
172
+ mcps: config.mcps
173
+ },
174
+ null,
175
+ 2
176
+ );
177
+ }
178
+ const sectionData = {
179
+ subscriptions: config.subscriptions,
180
+ models: config.models,
181
+ bmad: config.bmad,
182
+ features: config.features,
183
+ mcps: config.mcps
184
+ };
185
+ return JSON.stringify(
186
+ {
187
+ section,
188
+ data: sectionData[section] || null
189
+ },
190
+ null,
191
+ 2
192
+ );
193
+ }
194
+ });
195
+ }
196
+ function createGetContextTool(tracker, _config) {
197
+ return tool({
198
+ description: `Get the current story context without reloading from files.
199
+
200
+ Returns the cached context from the story tracker including:
201
+ - Current story ID and status
202
+ - When the story was started
203
+ - Recent activity history
204
+
205
+ Use this for quick status checks. Use athena_get_story to reload full context from files.`,
206
+ args: {},
207
+ async execute() {
208
+ const currentStory = tracker.getCurrentStory();
209
+ if (!currentStory) {
210
+ return JSON.stringify({
211
+ status: "no_active_story",
212
+ message: "No story is currently being tracked. Use athena_get_story to load a story."
213
+ });
214
+ }
215
+ const context = await tracker.getCurrentStoryContext();
216
+ const history = tracker.getHistory().slice(-10);
217
+ return JSON.stringify(
218
+ {
219
+ currentStory: {
220
+ id: currentStory.id,
221
+ status: currentStory.status,
222
+ startedAt: currentStory.startedAt,
223
+ completedAt: currentStory.completedAt
224
+ },
225
+ contextSummary: context,
226
+ recentHistory: history,
227
+ sessionId: tracker.getSessionId()
228
+ },
229
+ null,
230
+ 2
231
+ );
232
+ }
233
+ });
234
+ }
235
+ var BMAD_DIR_NAMES = ["docs", ".bmad", "bmad"];
236
+ var BMAD_V6_DEFAULTS = {
237
+ planningArtifacts: "docs/project-planning-artifacts",
238
+ implementationArtifacts: "docs/implementation-artifacts"};
239
+ var LEGACY_PATHS = {
240
+ docsDir: "docs",
241
+ sprintArtifacts: "docs/sprint-artifacts"
242
+ };
243
+ async function findBmadDir(startDir) {
244
+ let currentDir = startDir;
245
+ const visited = /* @__PURE__ */ new Set();
246
+ while (currentDir && !visited.has(currentDir)) {
247
+ visited.add(currentDir);
248
+ for (const dirName of BMAD_DIR_NAMES) {
249
+ const bmadPath = join(currentDir, dirName);
250
+ if (existsSync(bmadPath)) {
251
+ return bmadPath;
252
+ }
253
+ }
254
+ const parentDir = dirname(currentDir);
255
+ if (parentDir === currentDir) {
256
+ break;
257
+ }
258
+ currentDir = parentDir;
259
+ }
260
+ return null;
261
+ }
262
+ async function readBmadConfig(bmadDir) {
263
+ const configPath = join(bmadDir, "bmm", "config.yaml");
264
+ if (!existsSync(configPath)) {
265
+ return null;
266
+ }
267
+ try {
268
+ const content = await readFile(configPath, "utf-8");
269
+ return parse(content);
270
+ } catch {
271
+ return null;
272
+ }
273
+ }
274
+ function searchForFile(projectRoot, filename, searchPaths) {
275
+ for (const searchPath of searchPaths) {
276
+ const fullPath = join(projectRoot, searchPath, filename);
277
+ if (existsSync(fullPath)) {
278
+ return fullPath;
279
+ }
280
+ }
281
+ return join(projectRoot, searchPaths[0], filename);
282
+ }
283
+ async function getBmadPaths(startDir) {
284
+ const bmadDir = await findBmadDir(startDir);
285
+ const projectRoot = bmadDir ? dirname(bmadDir) : startDir;
286
+ let config = null;
287
+ if (bmadDir) {
288
+ config = await readBmadConfig(bmadDir);
289
+ }
290
+ const planningDir = config?.planning_artifacts || join(projectRoot, BMAD_V6_DEFAULTS.planningArtifacts);
291
+ const implementationDir = config?.implementation_artifacts || config?.sprint_artifacts || join(projectRoot, BMAD_V6_DEFAULTS.implementationArtifacts);
292
+ const storiesDir = join(implementationDir, "stories");
293
+ const sprintStatusSearchPaths = [
294
+ config?.implementation_artifacts || BMAD_V6_DEFAULTS.implementationArtifacts,
295
+ config?.sprint_artifacts || "docs/sprint-artifacts",
296
+ LEGACY_PATHS.sprintArtifacts,
297
+ LEGACY_PATHS.docsDir
298
+ ];
299
+ const sprintStatus = searchForFile(projectRoot, "sprint-status.yaml", sprintStatusSearchPaths);
300
+ const architectureSearchPaths = [
301
+ config?.planning_artifacts || BMAD_V6_DEFAULTS.planningArtifacts,
302
+ LEGACY_PATHS.docsDir
303
+ ];
304
+ const architecture = searchForFile(projectRoot, "architecture.md", architectureSearchPaths);
305
+ const prdSearchPaths = [
306
+ config?.planning_artifacts || BMAD_V6_DEFAULTS.planningArtifacts,
307
+ LEGACY_PATHS.docsDir
308
+ ];
309
+ const prd = searchForFile(projectRoot, "PRD.md", prdSearchPaths);
310
+ const epicsSearchPaths = [
311
+ config?.planning_artifacts || BMAD_V6_DEFAULTS.planningArtifacts,
312
+ LEGACY_PATHS.docsDir
313
+ ];
314
+ const epics = searchForFile(projectRoot, "epics.md", epicsSearchPaths);
315
+ return {
316
+ projectRoot,
317
+ bmadDir,
318
+ planningDir,
319
+ implementationDir,
320
+ storiesDir,
321
+ sprintStatus,
322
+ architecture,
323
+ prd,
324
+ epics
325
+ };
326
+ }
327
+ async function extractRelevantArchitecture(archPath, storyContent) {
328
+ if (!existsSync(archPath)) {
329
+ return "";
330
+ }
331
+ try {
332
+ const archContent = await readFile(archPath, "utf-8");
333
+ const sections = [];
334
+ const techStackMatch = archContent.match(/## Tech(?:nology)? Stack[\s\S]*?(?=\n## |$)/i);
335
+ if (techStackMatch) {
336
+ sections.push(techStackMatch[0].trim());
337
+ }
338
+ const patternsMatch = archContent.match(
339
+ /## (?:Patterns|Conventions|Standards|Coding Standards)[\s\S]*?(?=\n## |$)/i
340
+ );
341
+ if (patternsMatch) {
342
+ sections.push(patternsMatch[0].trim());
343
+ }
344
+ if (/data|database|model|schema|entity/i.test(storyContent)) {
345
+ const dataMatch = archContent.match(
346
+ /## (?:Data Model|Database|Schema|Data Layer)[\s\S]*?(?=\n## |$)/i
347
+ );
348
+ if (dataMatch) {
349
+ sections.push(dataMatch[0].trim());
350
+ }
351
+ }
352
+ if (/api|endpoint|route|rest|graphql/i.test(storyContent)) {
353
+ const apiMatch = archContent.match(
354
+ /## (?:API|Endpoints|Routes|REST|GraphQL)[\s\S]*?(?=\n## |$)/i
355
+ );
356
+ if (apiMatch) {
357
+ sections.push(apiMatch[0].trim());
358
+ }
359
+ }
360
+ if (/ui|component|view|page|screen|frontend/i.test(storyContent)) {
361
+ const uiMatch = archContent.match(
362
+ /## (?:UI|Components|Views|Frontend|User Interface)[\s\S]*?(?=\n## |$)/i
363
+ );
364
+ if (uiMatch) {
365
+ sections.push(uiMatch[0].trim());
366
+ }
367
+ }
368
+ return sections.length > 0 ? sections.join("\n\n---\n\n") : "See full architecture document for details.";
369
+ } catch (error) {
370
+ console.warn("[Athena] Failed to read architecture file:", error);
371
+ return "";
372
+ }
373
+ }
374
+ async function extractRelevantPRD(prdPath, storyContent) {
375
+ if (!existsSync(prdPath)) {
376
+ return "";
377
+ }
378
+ try {
379
+ const prdContent = await readFile(prdPath, "utf-8");
380
+ const frMatches = storyContent.match(/FR-?\d+/gi);
381
+ if (!frMatches || frMatches.length === 0) {
382
+ return "";
383
+ }
384
+ const uniqueFRs = [...new Set(frMatches.map((fr) => fr.toUpperCase()))];
385
+ const sections = [];
386
+ for (const fr of uniqueFRs) {
387
+ const normalizedFR = fr.replace("-", "-?");
388
+ const regex = new RegExp(`###+ ${normalizedFR}[:\\s][\\s\\S]*?(?=\\n###+ |$)`, "i");
389
+ const match = prdContent.match(regex);
390
+ if (match) {
391
+ sections.push(match[0].trim());
392
+ }
393
+ }
394
+ return sections.join("\n\n");
395
+ } catch (error) {
396
+ console.warn("[Athena] Failed to read PRD file:", error);
397
+ return "";
398
+ }
399
+ }
400
+ function generateImplementationInstructions(storyId) {
401
+ return `
402
+ ## Implementation Instructions for Story ${storyId}
403
+
404
+ You are implementing this story using OpenCode Athena's full capabilities.
405
+
406
+ ### Available Subagents
407
+ - **@oracle** - Use for complex debugging, architectural decisions, or strategic code choices
408
+ - **@librarian** - Use for finding implementation examples, researching patterns, exploring documentation
409
+ - **@explore** - Use for fast codebase searches and pattern matching
410
+ - **@frontend-ui-ux-engineer** - Use for UI component implementation (if applicable)
411
+
412
+ ### Available Tools
413
+ - **LSP Tools** - Use lsp_rename, lsp_find_references, lsp_code_actions for refactoring
414
+ - **AST-Grep** - Use ast_grep_search and ast_grep_replace for structural code changes
415
+
416
+ ### Quality Standards
417
+ 1. Meet ALL acceptance criteria listed in the story
418
+ 2. Follow architecture patterns exactly as specified
419
+ 3. Keep comments minimal - code should be self-documenting
420
+ 4. Run tests before marking complete
421
+ 5. Ensure no regressions in existing functionality
422
+
423
+ ### Completion
424
+ When implementation is complete:
425
+ 1. Call **athena_update_status** with status "completed" and a completion summary
426
+ 2. The sprint-status.yaml will be automatically updated
427
+ 3. Report what was implemented and any decisions made
428
+
429
+ ### If You Get Stuck
430
+ - Call @oracle for debugging help
431
+ - Call @librarian to find similar implementations
432
+ - Check the architecture document for patterns
433
+ `.trim();
434
+ }
435
+ var LOCK_EXT = ".lock";
436
+ var LOCK_TIMEOUT = 1e4;
437
+ var LOCK_RETRY_INTERVAL = 50;
438
+ var STALE_LOCK_AGE = 3e4;
439
+ async function acquireLock(filePath) {
440
+ const lockPath = `${filePath}${LOCK_EXT}`;
441
+ const startTime = Date.now();
442
+ const lockId = `${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`;
443
+ while (Date.now() - startTime < LOCK_TIMEOUT) {
444
+ try {
445
+ if (existsSync(lockPath)) {
446
+ const lockContent = await readFile(lockPath, "utf-8").catch(() => null);
447
+ if (lockContent) {
448
+ try {
449
+ const lockData2 = JSON.parse(lockContent);
450
+ const lockAge = Date.now() - lockData2.timestamp;
451
+ if (lockAge > STALE_LOCK_AGE) {
452
+ await rm(lockPath, { force: true });
453
+ }
454
+ } catch {
455
+ await rm(lockPath, { force: true });
456
+ }
457
+ }
458
+ }
459
+ const lockData = JSON.stringify({
460
+ id: lockId,
461
+ pid: process.pid,
462
+ timestamp: Date.now()
463
+ });
464
+ await writeFile(lockPath, lockData, { flag: "wx" });
465
+ return async () => {
466
+ try {
467
+ const currentContent = await readFile(lockPath, "utf-8").catch(() => null);
468
+ if (currentContent) {
469
+ const currentData = JSON.parse(currentContent);
470
+ if (currentData.id === lockId) {
471
+ await rm(lockPath, { force: true });
472
+ }
473
+ }
474
+ } catch {
475
+ }
476
+ };
477
+ } catch (error) {
478
+ const nodeError = error;
479
+ if (nodeError.code !== "EEXIST") {
480
+ throw error;
481
+ }
482
+ await sleep(LOCK_RETRY_INTERVAL);
483
+ }
484
+ }
485
+ throw new Error(`Failed to acquire lock for ${filePath} within ${LOCK_TIMEOUT}ms`);
486
+ }
487
+ function sleep(ms) {
488
+ return new Promise((resolve) => setTimeout(resolve, ms));
489
+ }
490
+ async function readYamlFile(filePath) {
491
+ if (!existsSync(filePath)) {
492
+ return null;
493
+ }
494
+ try {
495
+ const content = await readFile(filePath, "utf-8");
496
+ return parse(content);
497
+ } catch (error) {
498
+ console.warn(`[Athena] Failed to parse YAML file ${filePath}:`, error);
499
+ return null;
500
+ }
501
+ }
502
+ async function writeYamlFile(filePath, data) {
503
+ const content = stringify(data, {
504
+ indent: 2,
505
+ lineWidth: 120
506
+ });
507
+ await writeFile(filePath, content, "utf-8");
508
+ }
509
+ async function readSprintStatus(filePath) {
510
+ const raw = await readYamlFile(filePath);
511
+ if (!raw) {
512
+ return null;
513
+ }
514
+ return {
515
+ completed_stories: [],
516
+ pending_stories: [],
517
+ in_progress_stories: [],
518
+ blocked_stories: [],
519
+ ...raw
520
+ };
521
+ }
522
+ async function writeSprintStatus(filePath, status) {
523
+ const dir = dirname(filePath);
524
+ if (!existsSync(dir)) {
525
+ await mkdir(dir, { recursive: true });
526
+ }
527
+ const releaseLock = await acquireLock(filePath);
528
+ try {
529
+ const currentStatus = await readSprintStatus(filePath);
530
+ let statusToWrite = status;
531
+ if (currentStatus) {
532
+ if (status.last_modified && currentStatus.last_modified && currentStatus.last_modified !== status.last_modified) {
533
+ statusToWrite = mergeSprintStatus(currentStatus, status);
534
+ }
535
+ }
536
+ statusToWrite.last_modified = (/* @__PURE__ */ new Date()).toISOString();
537
+ await writeYamlFile(filePath, statusToWrite);
538
+ } finally {
539
+ await releaseLock();
540
+ }
541
+ }
542
+ function mergeSprintStatus(current, incoming) {
543
+ const merged = { ...current };
544
+ merged.story_updates = { ...current.story_updates };
545
+ if (incoming.story_updates) {
546
+ for (const [storyId, update] of Object.entries(incoming.story_updates)) {
547
+ const currentUpdate = merged.story_updates?.[storyId];
548
+ if (!currentUpdate || new Date(update.updated_at) > new Date(currentUpdate.updated_at)) {
549
+ merged.story_updates = merged.story_updates || {};
550
+ merged.story_updates[storyId] = update;
551
+ }
552
+ }
553
+ }
554
+ if (merged.story_updates) {
555
+ const statusArrays = {
556
+ completed_stories: /* @__PURE__ */ new Set(),
557
+ pending_stories: /* @__PURE__ */ new Set(),
558
+ in_progress_stories: /* @__PURE__ */ new Set(),
559
+ blocked_stories: /* @__PURE__ */ new Set(),
560
+ stories_needing_review: /* @__PURE__ */ new Set()
561
+ };
562
+ for (const story of current.completed_stories || []) {
563
+ statusArrays.completed_stories.add(story);
564
+ }
565
+ for (const story of current.pending_stories || []) {
566
+ statusArrays.pending_stories.add(story);
567
+ }
568
+ for (const story of current.in_progress_stories || []) {
569
+ statusArrays.in_progress_stories.add(story);
570
+ }
571
+ for (const story of current.blocked_stories || []) {
572
+ statusArrays.blocked_stories.add(story);
573
+ }
574
+ for (const [storyId, update] of Object.entries(merged.story_updates)) {
575
+ statusArrays.completed_stories.delete(storyId);
576
+ statusArrays.pending_stories.delete(storyId);
577
+ statusArrays.in_progress_stories.delete(storyId);
578
+ statusArrays.blocked_stories.delete(storyId);
579
+ statusArrays.stories_needing_review.delete(storyId);
580
+ switch (update.status) {
581
+ case "completed":
582
+ statusArrays.completed_stories.add(storyId);
583
+ break;
584
+ case "pending":
585
+ statusArrays.pending_stories.add(storyId);
586
+ break;
587
+ case "in_progress":
588
+ statusArrays.in_progress_stories.add(storyId);
589
+ break;
590
+ case "blocked":
591
+ statusArrays.blocked_stories.add(storyId);
592
+ break;
593
+ case "needs_review":
594
+ statusArrays.in_progress_stories.add(storyId);
595
+ statusArrays.stories_needing_review.add(storyId);
596
+ break;
597
+ }
598
+ }
599
+ merged.completed_stories = [...statusArrays.completed_stories];
600
+ merged.pending_stories = [...statusArrays.pending_stories];
601
+ merged.in_progress_stories = [...statusArrays.in_progress_stories];
602
+ merged.blocked_stories = [...statusArrays.blocked_stories];
603
+ merged.stories_needing_review = [...statusArrays.stories_needing_review];
604
+ }
605
+ if (incoming.current_story !== void 0) {
606
+ merged.current_story = incoming.current_story;
607
+ }
608
+ return merged;
609
+ }
610
+
611
+ // src/plugin/tools/get-story.ts
612
+ function createGetStoryTool(ctx, tracker, _config) {
613
+ return tool({
614
+ description: `Load the current BMAD story context for implementation.
615
+
616
+ Returns:
617
+ - Story file content with requirements and acceptance criteria
618
+ - Relevant architecture sections
619
+ - Sprint progress information
620
+ - Implementation instructions for using Sisyphus and subagents
621
+
622
+ Use this tool before starting story implementation to get full context.`,
623
+ args: {
624
+ storyId: tool.schema.string().optional().describe(
625
+ "Specific story ID (e.g., '2.3'). If omitted, loads the next pending story from sprint-status.yaml."
626
+ )
627
+ },
628
+ async execute(args) {
629
+ const result = await getStoryContext(ctx, tracker, args.storyId);
630
+ return JSON.stringify(result, null, 2);
631
+ }
632
+ });
633
+ }
634
+ async function getStoryContext(ctx, tracker, requestedStoryId) {
635
+ const paths = await getBmadPaths(ctx.directory);
636
+ if (!paths.bmadDir) {
637
+ return {
638
+ error: "No BMAD directory found",
639
+ suggestion: "Run 'npx bmad-method@alpha install' to set up BMAD in this project."
640
+ };
641
+ }
642
+ const sprint = await readSprintStatus(paths.sprintStatus);
643
+ if (!sprint) {
644
+ return {
645
+ error: "No sprint-status.yaml found",
646
+ suggestion: "Run the sprint-planning workflow with BMAD's SM agent first."
647
+ };
648
+ }
649
+ const storyId = requestedStoryId || findNextPendingStory(sprint);
650
+ if (!storyId) {
651
+ return {
652
+ error: "No pending stories found",
653
+ sprintProgress: {
654
+ completed: sprint.completed_stories.length,
655
+ total: sprint.completed_stories.length + sprint.pending_stories.length + sprint.in_progress_stories.length
656
+ },
657
+ suggestion: "All stories in current sprint are complete!"
658
+ };
659
+ }
660
+ const storyContent = await loadStoryFile(paths.storiesDir, storyId);
661
+ if (!storyContent) {
662
+ return {
663
+ error: `Story file not found for ${storyId}`,
664
+ suggestion: "Run 'create-story' workflow with BMAD's SM agent."
665
+ };
666
+ }
667
+ const archContent = await extractRelevantArchitecture(paths.architecture, storyContent);
668
+ const prdContent = await extractRelevantPRD(paths.prd, storyContent);
669
+ await tracker.setCurrentStory(storyId, {
670
+ content: storyContent,
671
+ status: "loading",
672
+ startedAt: (/* @__PURE__ */ new Date()).toISOString()
673
+ });
674
+ return {
675
+ storyId,
676
+ story: storyContent,
677
+ architecture: archContent || "No architecture document found.",
678
+ prd: prdContent || "No PRD document found.",
679
+ sprint: {
680
+ currentEpic: sprint.current_epic || "Unknown",
681
+ completedStories: sprint.completed_stories.length,
682
+ pendingStories: sprint.pending_stories.length,
683
+ blockedStories: sprint.blocked_stories.length
684
+ },
685
+ instructions: generateImplementationInstructions(storyId)
686
+ };
687
+ }
688
+ function findNextPendingStory(sprint) {
689
+ if (sprint.current_story) {
690
+ return sprint.current_story;
691
+ }
692
+ if (sprint.in_progress_stories.length > 0) {
693
+ return sprint.in_progress_stories[0];
694
+ }
695
+ if (sprint.pending_stories.length > 0) {
696
+ return sprint.pending_stories[0];
697
+ }
698
+ return null;
699
+ }
700
+ async function loadStoryFile(storiesDir, storyId) {
701
+ const possibleNames = [
702
+ `story-${storyId.replace(".", "-")}.md`,
703
+ // story-2-3.md
704
+ `story-${storyId}.md`,
705
+ // story-2.3.md
706
+ `${storyId}.md`
707
+ // 2.3.md
708
+ ];
709
+ for (const fileName of possibleNames) {
710
+ const filePath = join(storiesDir, fileName);
711
+ if (existsSync(filePath)) {
712
+ try {
713
+ return await readFile(filePath, "utf-8");
714
+ } catch {
715
+ }
716
+ }
717
+ }
718
+ return null;
719
+ }
720
+ function createParallelTool() {
721
+ return tool({
722
+ description: `Execute multiple independent stories in parallel using background agents.
723
+
724
+ NOTE: This feature is not yet implemented. It requires integration with oh-my-opencode's background agent system.
725
+
726
+ When implemented, this tool will:
727
+ 1. Validate that the specified stories have no file conflicts
728
+ 2. Mark all stories as in_progress in sprint-status.yaml
729
+ 3. Spawn background agents to implement each story
730
+ 4. Coordinate completion and report results`,
731
+ args: {
732
+ storyIds: tool.schema.array(tool.schema.string()).describe("Array of story IDs to execute in parallel"),
733
+ waitForCompletion: tool.schema.boolean().optional().describe("Whether to wait for all stories to complete (default: true)")
734
+ },
735
+ async execute(args) {
736
+ return JSON.stringify(
737
+ {
738
+ error: "Not implemented",
739
+ message: "Parallel story execution is not yet implemented. This feature requires integration with oh-my-opencode's background agent system.",
740
+ requestedStories: args.storyIds,
741
+ suggestion: "For now, implement stories sequentially using /athena-dev for each story."
742
+ },
743
+ null,
744
+ 2
745
+ );
746
+ }
747
+ });
748
+ }
749
+ function createUpdateStatusTool(ctx, tracker, config) {
750
+ return tool({
751
+ description: `Update the BMAD sprint status for a story.
752
+
753
+ Call this tool when:
754
+ - Starting a story (status: "in_progress")
755
+ - Completing a story (status: "completed") - requires completionSummary
756
+ - Blocking on an issue (status: "blocked") - requires notes explaining blocker
757
+ - Requesting review (status: "needs_review")
758
+
759
+ The sprint-status.yaml file will be automatically updated.`,
760
+ args: {
761
+ storyId: tool.schema.string().describe("The story ID (e.g., '2.3')"),
762
+ status: tool.schema.enum(["in_progress", "completed", "blocked", "needs_review"]).describe("The new status for the story"),
763
+ notes: tool.schema.string().optional().describe("Notes about the status change (required for 'blocked' status)"),
764
+ completionSummary: tool.schema.string().optional().describe("Summary of what was implemented (required for 'completed' status)")
765
+ },
766
+ async execute(args) {
767
+ const result = await updateStoryStatus(ctx, tracker, config, args);
768
+ return JSON.stringify(result, null, 2);
769
+ }
770
+ });
771
+ }
772
+ async function updateStoryStatus(ctx, tracker, config, args) {
773
+ const { storyId, status, notes, completionSummary } = args;
774
+ if (status === "completed" && !completionSummary) {
775
+ return {
776
+ error: "completionSummary is required when marking a story completed"
777
+ };
778
+ }
779
+ if (status === "blocked" && !notes) {
780
+ return {
781
+ error: "notes are required when blocking a story (explain the blocker)"
782
+ };
783
+ }
784
+ const paths = await getBmadPaths(ctx.directory);
785
+ if (!paths.bmadDir) {
786
+ return { error: "No BMAD directory found" };
787
+ }
788
+ if (!existsSync(paths.sprintStatus)) {
789
+ return { error: "No sprint-status.yaml found" };
790
+ }
791
+ const sprint = await readSprintStatus(paths.sprintStatus);
792
+ if (!sprint) {
793
+ return { error: "Failed to read sprint-status.yaml" };
794
+ }
795
+ const now = (/* @__PURE__ */ new Date()).toISOString();
796
+ removeFromAllArrays(sprint, storyId);
797
+ switch (status) {
798
+ case "in_progress":
799
+ addToArrayIfNotPresent(sprint.in_progress_stories, storyId);
800
+ sprint.current_story = storyId;
801
+ break;
802
+ case "completed":
803
+ addToArrayIfNotPresent(sprint.completed_stories, storyId);
804
+ if (sprint.current_story === storyId) {
805
+ sprint.current_story = null;
806
+ }
807
+ break;
808
+ case "blocked":
809
+ addToArrayIfNotPresent(sprint.blocked_stories, storyId);
810
+ if (sprint.current_story === storyId) {
811
+ sprint.current_story = null;
812
+ }
813
+ break;
814
+ case "needs_review":
815
+ addToArrayIfNotPresent(sprint.in_progress_stories, storyId);
816
+ sprint.stories_needing_review = sprint.stories_needing_review || [];
817
+ addToArrayIfNotPresent(sprint.stories_needing_review, storyId);
818
+ break;
819
+ }
820
+ sprint.story_updates = sprint.story_updates || {};
821
+ sprint.story_updates[storyId] = {
822
+ status,
823
+ updated_at: now,
824
+ ...notes && { notes },
825
+ ...completionSummary && { completion_summary: completionSummary }
826
+ };
827
+ await writeSprintStatus(paths.sprintStatus, sprint);
828
+ await tracker.updateStoryStatus(storyId, status);
829
+ if (config.features?.notifications && status === "completed") {
830
+ await sendNotification(`Story ${storyId} completed!`, "OpenCode Athena", ctx.$);
831
+ }
832
+ const totalStories = sprint.completed_stories.length + sprint.pending_stories.length + sprint.in_progress_stories.length + sprint.blocked_stories.length;
833
+ const percentComplete = totalStories > 0 ? Math.round(sprint.completed_stories.length / totalStories * 100) : 0;
834
+ return {
835
+ success: true,
836
+ storyId,
837
+ newStatus: status,
838
+ updatedAt: now,
839
+ sprintProgress: {
840
+ completed: sprint.completed_stories.length,
841
+ inProgress: sprint.in_progress_stories.length,
842
+ pending: sprint.pending_stories.length,
843
+ blocked: sprint.blocked_stories.length,
844
+ total: totalStories,
845
+ percentComplete
846
+ },
847
+ nextStory: status === "completed" ? sprint.pending_stories[0] || null : null
848
+ };
849
+ }
850
+ function removeFromAllArrays(sprint, storyId) {
851
+ sprint.completed_stories = [...new Set(sprint.completed_stories.filter((s) => s !== storyId))];
852
+ sprint.pending_stories = [...new Set(sprint.pending_stories.filter((s) => s !== storyId))];
853
+ sprint.in_progress_stories = [
854
+ ...new Set(sprint.in_progress_stories.filter((s) => s !== storyId))
855
+ ];
856
+ sprint.blocked_stories = [...new Set(sprint.blocked_stories.filter((s) => s !== storyId))];
857
+ if (sprint.stories_needing_review) {
858
+ sprint.stories_needing_review = [
859
+ ...new Set(sprint.stories_needing_review.filter((s) => s !== storyId))
860
+ ];
861
+ }
862
+ }
863
+ function addToArrayIfNotPresent(array, storyId) {
864
+ if (!array.includes(storyId)) {
865
+ array.push(storyId);
866
+ }
867
+ }
868
+
869
+ // src/plugin/tools/index.ts
870
+ function createTools(ctx, tracker, config) {
871
+ return {
872
+ athena_get_story: createGetStoryTool(ctx, tracker),
873
+ athena_update_status: createUpdateStatusTool(ctx, tracker, config),
874
+ athena_get_context: createGetContextTool(tracker),
875
+ athena_parallel: createParallelTool(),
876
+ athena_config: createConfigTool(config)
877
+ };
878
+ }
879
+ var StoryTracker = class {
880
+ state;
881
+ stateFilePath;
882
+ projectDir;
883
+ constructor(projectDir) {
884
+ this.projectDir = projectDir;
885
+ this.stateFilePath = CONFIG_PATHS.stateFile;
886
+ this.state = {
887
+ currentStory: null,
888
+ sessionId: crypto.randomUUID(),
889
+ projectDir,
890
+ history: []
891
+ };
892
+ }
893
+ /**
894
+ * Initialize the tracker by loading existing state
895
+ */
896
+ async initialize() {
897
+ if (existsSync(this.stateFilePath)) {
898
+ try {
899
+ const content = await readFile(this.stateFilePath, "utf-8");
900
+ const savedState = JSON.parse(content);
901
+ if (savedState.projectDir === this.projectDir) {
902
+ this.state = {
903
+ ...savedState,
904
+ sessionId: crypto.randomUUID()
905
+ // Always generate new session ID
906
+ };
907
+ }
908
+ } catch {
909
+ }
910
+ }
911
+ }
912
+ /**
913
+ * Set the current story being worked on
914
+ */
915
+ async setCurrentStory(storyId, data) {
916
+ this.state.currentStory = { id: storyId, ...data };
917
+ this.addHistoryEntry(storyId, data.status);
918
+ await this.saveState();
919
+ }
920
+ /**
921
+ * Update the status of a story
922
+ */
923
+ async updateStoryStatus(storyId, status) {
924
+ if (this.state.currentStory?.id === storyId) {
925
+ this.state.currentStory.status = status;
926
+ if (status === "completed") {
927
+ this.state.currentStory.completedAt = (/* @__PURE__ */ new Date()).toISOString();
928
+ }
929
+ }
930
+ this.addHistoryEntry(storyId, status);
931
+ await this.saveState();
932
+ }
933
+ /**
934
+ * Get the current story being tracked
935
+ */
936
+ getCurrentStory() {
937
+ return this.state.currentStory;
938
+ }
939
+ /**
940
+ * Get a formatted context string for the current story
941
+ * Used for compaction hooks to preserve context
942
+ */
943
+ async getCurrentStoryContext() {
944
+ if (!this.state.currentStory) {
945
+ return null;
946
+ }
947
+ const story = this.state.currentStory;
948
+ const recentHistory = this.state.history.slice(-5).map((h) => `- ${h.storyId}: ${h.status} at ${h.timestamp}`).join("\n");
949
+ return `
950
+ Current Story: ${story.id}
951
+ Status: ${story.status}
952
+ Started: ${story.startedAt}
953
+ ${story.completedAt ? `Completed: ${story.completedAt}` : ""}
954
+
955
+ Recent Activity:
956
+ ${recentHistory}
957
+ `.trim();
958
+ }
959
+ /**
960
+ * Clear the current story (e.g., when completed or cancelled)
961
+ */
962
+ async clearCurrentStory() {
963
+ this.state.currentStory = null;
964
+ await this.saveState();
965
+ }
966
+ /**
967
+ * Get the current session ID
968
+ */
969
+ getSessionId() {
970
+ return this.state.sessionId;
971
+ }
972
+ /**
973
+ * Get the history of status changes
974
+ */
975
+ getHistory() {
976
+ return this.state.history;
977
+ }
978
+ /**
979
+ * Add an entry to the history
980
+ */
981
+ addHistoryEntry(storyId, status) {
982
+ this.state.history.push({
983
+ storyId,
984
+ status,
985
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
986
+ });
987
+ if (this.state.history.length > 100) {
988
+ this.state.history = this.state.history.slice(-100);
989
+ }
990
+ }
991
+ /**
992
+ * Save state to disk
993
+ */
994
+ async saveState() {
995
+ try {
996
+ const dir = dirname(this.stateFilePath);
997
+ if (!existsSync(dir)) {
998
+ await mkdir(dir, { recursive: true });
999
+ }
1000
+ await writeFile(this.stateFilePath, JSON.stringify(this.state, null, 2), "utf-8");
1001
+ } catch (error) {
1002
+ console.warn("[Athena] Failed to save tracker state:", error);
1003
+ }
1004
+ }
1005
+ };
1006
+ var SubscriptionSchema = z.object({
1007
+ claude: z.object({
1008
+ enabled: z.boolean(),
1009
+ tier: z.enum(["max5x", "max20x", "pro", "none"])
1010
+ }),
1011
+ openai: z.object({
1012
+ enabled: z.boolean()
1013
+ }),
1014
+ google: z.object({
1015
+ enabled: z.boolean(),
1016
+ authMethod: z.enum(["antigravity", "personal", "api", "none"])
1017
+ })
1018
+ });
1019
+ var BmadConfigSchema = z.object({
1020
+ defaultTrack: z.enum(["quick-flow", "bmad-method", "enterprise"]),
1021
+ autoStatusUpdate: z.boolean(),
1022
+ parallelStoryLimit: z.number().int().min(0).max(10)
1023
+ });
1024
+ var FeaturesSchema = z.object({
1025
+ bmadBridge: z.boolean(),
1026
+ autoStatus: z.boolean(),
1027
+ parallelExecution: z.boolean(),
1028
+ notifications: z.boolean(),
1029
+ contextMonitor: z.boolean(),
1030
+ commentChecker: z.boolean(),
1031
+ lspTools: z.boolean()
1032
+ });
1033
+ var McpsSchema = z.object({
1034
+ context7: z.boolean(),
1035
+ exa: z.boolean(),
1036
+ grepApp: z.boolean()
1037
+ });
1038
+ var ModelsSchema = z.object({
1039
+ sisyphus: z.string().describe("Model for main orchestrator agent"),
1040
+ oracle: z.string().describe("Model for debugging/reasoning agent"),
1041
+ librarian: z.string().describe("Model for research/documentation agent"),
1042
+ frontend: z.string().optional().describe("Model for UI/UX agent"),
1043
+ documentWriter: z.string().optional().describe("Model for documentation generation agent"),
1044
+ multimodalLooker: z.string().optional().describe("Model for image analysis agent")
1045
+ });
1046
+ var AthenaConfigSchema = z.object({
1047
+ $schema: z.string().optional(),
1048
+ version: z.string(),
1049
+ subscriptions: SubscriptionSchema,
1050
+ models: ModelsSchema,
1051
+ bmad: BmadConfigSchema,
1052
+ features: FeaturesSchema,
1053
+ mcps: McpsSchema
1054
+ });
1055
+ z.object({
1056
+ storyId: z.string().optional().describe(
1057
+ "Specific story ID (e.g., '2.3'). If omitted, loads the next pending story from sprint-status.yaml."
1058
+ )
1059
+ });
1060
+ z.object({
1061
+ storyId: z.string().describe("The story ID (e.g., '2.3')"),
1062
+ status: z.enum(["in_progress", "completed", "blocked", "needs_review"]).describe("The new status for the story"),
1063
+ notes: z.string().optional().describe("Notes about the status change (required for 'blocked' status)"),
1064
+ completionSummary: z.string().optional().describe("Summary of what was implemented (required for 'completed' status)")
1065
+ });
1066
+ z.object({
1067
+ includeArchitecture: z.boolean().optional().default(true),
1068
+ includePrd: z.boolean().optional().default(false),
1069
+ includeSprintStatus: z.boolean().optional().default(true)
1070
+ });
1071
+ z.object({
1072
+ storyIds: z.array(z.string()).describe("Array of story IDs to implement in parallel"),
1073
+ maxConcurrent: z.number().int().min(1).max(5).optional().default(3)
1074
+ });
1075
+ z.object({
1076
+ action: z.enum(["get", "set", "reset"]).describe("Configuration action to perform"),
1077
+ key: z.string().optional().describe("Configuration key (dot notation, e.g., 'bmad.autoStatusUpdate')"),
1078
+ value: z.unknown().optional().describe("Value to set (for 'set' action)")
1079
+ });
1080
+ var StoryStatusEnum = z.enum([
1081
+ "pending",
1082
+ "in_progress",
1083
+ "completed",
1084
+ "blocked",
1085
+ "needs_review"
1086
+ ]);
1087
+ z.enum([
1088
+ "pending",
1089
+ "in_progress",
1090
+ "completed",
1091
+ "blocked",
1092
+ "needs_review",
1093
+ "loading"
1094
+ ]);
1095
+ z.object({
1096
+ sprint_number: z.number().int().optional(),
1097
+ current_epic: z.string().optional(),
1098
+ current_story: z.string().nullable().optional(),
1099
+ completed_stories: z.array(z.string()).default([]),
1100
+ pending_stories: z.array(z.string()).default([]),
1101
+ in_progress_stories: z.array(z.string()).default([]),
1102
+ blocked_stories: z.array(z.string()).default([]),
1103
+ stories_needing_review: z.array(z.string()).optional(),
1104
+ story_updates: z.record(
1105
+ z.object({
1106
+ status: StoryStatusEnum,
1107
+ updated_at: z.string(),
1108
+ notes: z.string().optional(),
1109
+ completion_summary: z.string().optional()
1110
+ })
1111
+ ).optional(),
1112
+ last_modified: z.string().optional()
1113
+ });
1114
+
1115
+ // src/plugin/utils/config-loader.ts
1116
+ async function loadAthenaConfig(projectDir) {
1117
+ const localConfigPath = join(projectDir, ".opencode", "athena.json");
1118
+ const localConfig = await loadConfigFile(localConfigPath);
1119
+ const globalConfig = await loadConfigFile(CONFIG_PATHS.globalAthenaConfig);
1120
+ const merged = mergeConfigs(getDefaultConfig(), globalConfig, localConfig);
1121
+ return validateConfig(merged);
1122
+ }
1123
+ async function loadConfigFile(filePath) {
1124
+ if (!existsSync(filePath)) {
1125
+ return null;
1126
+ }
1127
+ try {
1128
+ const content = await readFile(filePath, "utf-8");
1129
+ const parsed = JSON.parse(content);
1130
+ return validatePartialConfig(parsed, filePath);
1131
+ } catch (error) {
1132
+ if (error instanceof SyntaxError) {
1133
+ console.warn(`[Athena] Invalid JSON in config file ${filePath}:`, error.message);
1134
+ } else {
1135
+ console.warn(`[Athena] Failed to load config from ${filePath}:`, error);
1136
+ }
1137
+ return null;
1138
+ }
1139
+ }
1140
+ function validatePartialConfig(config, filePath) {
1141
+ if (typeof config !== "object" || config === null) {
1142
+ console.warn(`[Athena] Config file ${filePath} must be an object`);
1143
+ return null;
1144
+ }
1145
+ const result = {};
1146
+ const configObj = config;
1147
+ if ("version" in configObj && typeof configObj.version === "string") {
1148
+ result.version = configObj.version;
1149
+ }
1150
+ if ("$schema" in configObj && typeof configObj.$schema === "string") {
1151
+ result.$schema = configObj.$schema;
1152
+ }
1153
+ if ("subscriptions" in configObj && typeof configObj.subscriptions === "object") {
1154
+ const subs = configObj.subscriptions;
1155
+ result.subscriptions = {};
1156
+ if (subs.claude && typeof subs.claude === "object") {
1157
+ const claude = subs.claude;
1158
+ result.subscriptions.claude = {
1159
+ enabled: typeof claude.enabled === "boolean" ? claude.enabled : false,
1160
+ tier: isValidTier(claude.tier) ? claude.tier : "none"
1161
+ };
1162
+ }
1163
+ if (subs.openai && typeof subs.openai === "object") {
1164
+ const openai = subs.openai;
1165
+ result.subscriptions.openai = {
1166
+ enabled: typeof openai.enabled === "boolean" ? openai.enabled : false
1167
+ };
1168
+ }
1169
+ if (subs.google && typeof subs.google === "object") {
1170
+ const google = subs.google;
1171
+ result.subscriptions.google = {
1172
+ enabled: typeof google.enabled === "boolean" ? google.enabled : false,
1173
+ authMethod: isValidAuthMethod(google.authMethod) ? google.authMethod : "none"
1174
+ };
1175
+ }
1176
+ }
1177
+ if ("models" in configObj && typeof configObj.models === "object") {
1178
+ const models = configObj.models;
1179
+ result.models = {};
1180
+ for (const key of [
1181
+ "sisyphus",
1182
+ "oracle",
1183
+ "librarian",
1184
+ "frontend",
1185
+ "documentWriter",
1186
+ "multimodalLooker"
1187
+ ]) {
1188
+ if (key in models && typeof models[key] === "string") {
1189
+ result.models[key] = models[key];
1190
+ }
1191
+ }
1192
+ }
1193
+ if ("bmad" in configObj && typeof configObj.bmad === "object") {
1194
+ const bmad = configObj.bmad;
1195
+ result.bmad = {};
1196
+ if (isValidTrack(bmad.defaultTrack)) {
1197
+ result.bmad.defaultTrack = bmad.defaultTrack;
1198
+ }
1199
+ if (typeof bmad.autoStatusUpdate === "boolean") {
1200
+ result.bmad.autoStatusUpdate = bmad.autoStatusUpdate;
1201
+ }
1202
+ if (typeof bmad.parallelStoryLimit === "number") {
1203
+ result.bmad.parallelStoryLimit = Math.max(0, Math.min(10, bmad.parallelStoryLimit));
1204
+ }
1205
+ }
1206
+ if ("features" in configObj && typeof configObj.features === "object") {
1207
+ const features = configObj.features;
1208
+ result.features = {};
1209
+ for (const key of [
1210
+ "bmadBridge",
1211
+ "autoStatus",
1212
+ "parallelExecution",
1213
+ "notifications",
1214
+ "contextMonitor",
1215
+ "commentChecker",
1216
+ "lspTools"
1217
+ ]) {
1218
+ if (key in features && typeof features[key] === "boolean") {
1219
+ result.features[key] = features[key];
1220
+ }
1221
+ }
1222
+ }
1223
+ if ("mcps" in configObj && typeof configObj.mcps === "object") {
1224
+ const mcps = configObj.mcps;
1225
+ result.mcps = {};
1226
+ for (const key of ["context7", "exa", "grepApp"]) {
1227
+ if (key in mcps && typeof mcps[key] === "boolean") {
1228
+ result.mcps[key] = mcps[key];
1229
+ }
1230
+ }
1231
+ }
1232
+ return result;
1233
+ }
1234
+ function isValidTier(value) {
1235
+ return typeof value === "string" && ["max5x", "max20x", "pro", "none"].includes(value);
1236
+ }
1237
+ function isValidAuthMethod(value) {
1238
+ return typeof value === "string" && ["antigravity", "personal", "api", "none"].includes(value);
1239
+ }
1240
+ function isValidTrack(value) {
1241
+ return typeof value === "string" && ["quick-flow", "bmad-method", "enterprise"].includes(value);
1242
+ }
1243
+ function validateConfig(config) {
1244
+ const result = AthenaConfigSchema.safeParse(config);
1245
+ if (!result.success) {
1246
+ console.warn("[Athena] Configuration validation warnings:");
1247
+ for (const error of result.error.errors) {
1248
+ console.warn(` - ${error.path.join(".")}: ${error.message}`);
1249
+ }
1250
+ return config;
1251
+ }
1252
+ return result.data;
1253
+ }
1254
+ function getDefaultConfig() {
1255
+ return {
1256
+ version: "0.0.1",
1257
+ subscriptions: {
1258
+ claude: { enabled: false, tier: "none" },
1259
+ openai: { enabled: false },
1260
+ google: { enabled: false, authMethod: "none" }
1261
+ },
1262
+ models: {
1263
+ sisyphus: "anthropic/claude-sonnet-4",
1264
+ oracle: "anthropic/claude-sonnet-4",
1265
+ librarian: "anthropic/claude-sonnet-4"
1266
+ },
1267
+ bmad: {
1268
+ defaultTrack: DEFAULTS.defaultTrack,
1269
+ autoStatusUpdate: DEFAULTS.autoStatusUpdate,
1270
+ parallelStoryLimit: DEFAULTS.parallelStoryLimit
1271
+ },
1272
+ features: { ...DEFAULTS.features },
1273
+ mcps: { ...DEFAULTS.mcps }
1274
+ };
1275
+ }
1276
+ function mergeConfigs(...configs) {
1277
+ const result = getDefaultConfig();
1278
+ for (const config of configs) {
1279
+ if (!config) continue;
1280
+ if (config.version) result.version = config.version;
1281
+ if (config.subscriptions) {
1282
+ if (config.subscriptions.claude) {
1283
+ result.subscriptions.claude = {
1284
+ ...result.subscriptions.claude,
1285
+ ...config.subscriptions.claude
1286
+ };
1287
+ }
1288
+ if (config.subscriptions.openai) {
1289
+ result.subscriptions.openai = {
1290
+ ...result.subscriptions.openai,
1291
+ ...config.subscriptions.openai
1292
+ };
1293
+ }
1294
+ if (config.subscriptions.google) {
1295
+ result.subscriptions.google = {
1296
+ ...result.subscriptions.google,
1297
+ ...config.subscriptions.google
1298
+ };
1299
+ }
1300
+ }
1301
+ if (config.models) {
1302
+ result.models = { ...result.models, ...config.models };
1303
+ }
1304
+ if (config.bmad) {
1305
+ result.bmad = { ...result.bmad, ...config.bmad };
1306
+ }
1307
+ if (config.features) {
1308
+ result.features = { ...result.features, ...config.features };
1309
+ }
1310
+ if (config.mcps) {
1311
+ result.mcps = { ...result.mcps, ...config.mcps };
1312
+ }
1313
+ }
1314
+ return result;
1315
+ }
1316
+
1317
+ // src/plugin/index.ts
1318
+ var OpenCodeAthena = async (ctx) => {
1319
+ const { directory } = ctx;
1320
+ const config = await loadAthenaConfig(directory);
1321
+ const tracker = new StoryTracker(directory);
1322
+ await tracker.initialize();
1323
+ const tools = createTools(ctx, tracker, config);
1324
+ const sessionHooks = createSessionHooks(ctx, tracker, config);
1325
+ const toolHooks = createToolHooks();
1326
+ const compactionHook = createCompactionHook(tracker);
1327
+ return {
1328
+ // Custom tools for BMAD integration
1329
+ tool: tools,
1330
+ // Session event handlers
1331
+ event: sessionHooks,
1332
+ // Tool execution hooks (stubs for now)
1333
+ "tool.execute.before": toolHooks.before,
1334
+ "tool.execute.after": toolHooks.after,
1335
+ // Compaction hook to preserve BMAD context
1336
+ "experimental.session.compacting": compactionHook
1337
+ };
1338
+ };
1339
+ var plugin_default = OpenCodeAthena;
1340
+
1341
+ export { OpenCodeAthena, plugin_default as default };
1342
+ //# sourceMappingURL=index.js.map
1343
+ //# sourceMappingURL=index.js.map