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