opencode-athena 0.1.1 → 0.2.0
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/README.md +75 -4
- package/commands/athena-review-story.md +384 -0
- package/dist/index.js +356 -2
- package/dist/index.js.map +1 -1
- package/dist/plugin/index.js +356 -2
- package/dist/plugin/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@ import { homedir, platform } from 'os';
|
|
|
2
2
|
import { tool } from '@opencode-ai/plugin';
|
|
3
3
|
import { join, dirname } from 'path';
|
|
4
4
|
import { existsSync } from 'fs';
|
|
5
|
-
import { readFile, mkdir, writeFile, rm } from 'fs/promises';
|
|
5
|
+
import { readFile, mkdir, writeFile, readdir, rm } from 'fs/promises';
|
|
6
6
|
import { parse, stringify } from 'yaml';
|
|
7
7
|
import { z } from 'zod';
|
|
8
8
|
|
|
@@ -764,6 +764,359 @@ When implemented, this tool will:
|
|
|
764
764
|
}
|
|
765
765
|
});
|
|
766
766
|
}
|
|
767
|
+
function createReviewStoryTool(ctx, config) {
|
|
768
|
+
return tool({
|
|
769
|
+
description: `Run a party review on BMAD stories to find security, logic, best practice, and performance gaps.
|
|
770
|
+
|
|
771
|
+
This command generates a comprehensive review document BEFORE development starts, catching issues when they're cheap to fix (in markdown, not code).
|
|
772
|
+
|
|
773
|
+
The command is smart about argument types:
|
|
774
|
+
- Epic: "2", "epic-2" \u2192 Reviews all stories in the epic
|
|
775
|
+
- Story: "2.3", "story-2-3" \u2192 Deep dive on a single story
|
|
776
|
+
- Path: "docs/stories/story-2-3.md" \u2192 Review specific file
|
|
777
|
+
- Flag: --thorough \u2192 Force advanced model (ignores complexity detection)
|
|
778
|
+
|
|
779
|
+
Returns:
|
|
780
|
+
- Review document path (saved to docs/reviews/)
|
|
781
|
+
- Structured findings by severity and category
|
|
782
|
+
- Recommendations for next steps
|
|
783
|
+
|
|
784
|
+
Use this tool after story creation but before development to improve story quality.`,
|
|
785
|
+
args: {
|
|
786
|
+
identifier: tool.schema.string().describe("Epic number (e.g., '2'), Story ID (e.g., '2.3'), or file path. Required."),
|
|
787
|
+
thorough: tool.schema.boolean().optional().describe("Force advanced model for review (default: auto-detect based on complexity)")
|
|
788
|
+
},
|
|
789
|
+
async execute(args) {
|
|
790
|
+
const result = await executePartyReview(ctx, config, args.identifier, args.thorough);
|
|
791
|
+
return JSON.stringify(result, null, 2);
|
|
792
|
+
}
|
|
793
|
+
});
|
|
794
|
+
}
|
|
795
|
+
async function executePartyReview(ctx, config, identifier, forceAdvancedModel) {
|
|
796
|
+
const paths = await getBmadPaths(ctx.directory);
|
|
797
|
+
if (!paths.bmadDir) {
|
|
798
|
+
return {
|
|
799
|
+
success: false,
|
|
800
|
+
scope: "story",
|
|
801
|
+
identifier,
|
|
802
|
+
error: "No BMAD directory found",
|
|
803
|
+
suggestion: "Run 'npx bmad-method@alpha install' to set up BMAD in this project."
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
const scope = detectReviewScope(identifier);
|
|
807
|
+
const reviewsDir = join(paths.bmadDir, "reviews");
|
|
808
|
+
await ensureReviewsDirectory(reviewsDir);
|
|
809
|
+
if (scope === "epic") {
|
|
810
|
+
return await executeEpicReview(ctx, config, paths, identifier, reviewsDir, forceAdvancedModel);
|
|
811
|
+
}
|
|
812
|
+
return await executeStoryReview(ctx, config, paths, identifier, reviewsDir, forceAdvancedModel);
|
|
813
|
+
}
|
|
814
|
+
function detectReviewScope(identifier) {
|
|
815
|
+
const isFilePath = identifier.includes("/") || identifier.endsWith(".md");
|
|
816
|
+
if (isFilePath) {
|
|
817
|
+
return "story";
|
|
818
|
+
}
|
|
819
|
+
const cleanId = identifier.replace(/^(epic|story)-/, "");
|
|
820
|
+
const isEpic = !cleanId.includes(".") && !cleanId.includes("-");
|
|
821
|
+
return isEpic ? "epic" : "story";
|
|
822
|
+
}
|
|
823
|
+
async function ensureReviewsDirectory(reviewsDir) {
|
|
824
|
+
if (!existsSync(reviewsDir)) {
|
|
825
|
+
await mkdir(reviewsDir, { recursive: true });
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
async function executeEpicReview(_ctx, config, paths, identifier, reviewsDir, forceAdvancedModel) {
|
|
829
|
+
const epicNumber = identifier.replace(/^epic-/, "");
|
|
830
|
+
const stories = await findStoriesInEpic(paths.storiesDir, epicNumber);
|
|
831
|
+
if (stories.length === 0) {
|
|
832
|
+
return {
|
|
833
|
+
success: false,
|
|
834
|
+
scope: "epic",
|
|
835
|
+
identifier,
|
|
836
|
+
error: `No stories found for Epic ${epicNumber}`,
|
|
837
|
+
suggestion: `Check that story files exist in ${paths.storiesDir} with format story-${epicNumber}-*.md`
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
const storyContents = await Promise.all(
|
|
841
|
+
stories.map(async (storyId) => ({
|
|
842
|
+
id: storyId,
|
|
843
|
+
content: await loadStoryFile2(paths.storiesDir, storyId)
|
|
844
|
+
}))
|
|
845
|
+
);
|
|
846
|
+
const architectureContent = await loadArchitecture(paths.architecture);
|
|
847
|
+
const selectedModel = forceAdvancedModel ? config.models.oracle : config.models.oracle;
|
|
848
|
+
const oraclePrompt = buildEpicReviewPrompt(epicNumber, storyContents, architectureContent);
|
|
849
|
+
return {
|
|
850
|
+
success: true,
|
|
851
|
+
scope: "epic",
|
|
852
|
+
identifier: epicNumber,
|
|
853
|
+
oraclePrompt,
|
|
854
|
+
storiesContent: storyContents,
|
|
855
|
+
architectureContent,
|
|
856
|
+
selectedModel,
|
|
857
|
+
reviewsDir
|
|
858
|
+
};
|
|
859
|
+
}
|
|
860
|
+
async function executeStoryReview(_ctx, config, paths, identifier, reviewsDir, forceAdvancedModel) {
|
|
861
|
+
const storyId = normalizeStoryId(identifier);
|
|
862
|
+
const storyContent = await loadStoryFile2(paths.storiesDir, storyId);
|
|
863
|
+
if (!storyContent) {
|
|
864
|
+
return {
|
|
865
|
+
success: false,
|
|
866
|
+
scope: "story",
|
|
867
|
+
identifier: storyId,
|
|
868
|
+
error: `Story ${storyId} not found`,
|
|
869
|
+
suggestion: `Check that the story file exists at ${paths.storiesDir}/story-${storyId.replace(".", "-")}.md`
|
|
870
|
+
};
|
|
871
|
+
}
|
|
872
|
+
const existingReviews = await findExistingReviews(reviewsDir, storyId);
|
|
873
|
+
const epicReview = existingReviews.find((r) => r.type === "epic");
|
|
874
|
+
const architectureContent = await loadArchitecture(paths.architecture);
|
|
875
|
+
const complexity = await analyzeStoryComplexity(storyContent);
|
|
876
|
+
const selectedModel = forceAdvancedModel ? config.models.oracle : selectReviewModel(config, complexity);
|
|
877
|
+
const oraclePrompt = buildFocusedReviewPrompt(
|
|
878
|
+
storyId,
|
|
879
|
+
storyContent,
|
|
880
|
+
architectureContent,
|
|
881
|
+
epicReview
|
|
882
|
+
);
|
|
883
|
+
return {
|
|
884
|
+
success: true,
|
|
885
|
+
scope: "story",
|
|
886
|
+
identifier: storyId,
|
|
887
|
+
oraclePrompt,
|
|
888
|
+
storiesContent: [{ id: storyId, content: storyContent }],
|
|
889
|
+
architectureContent,
|
|
890
|
+
existingReviews,
|
|
891
|
+
complexity,
|
|
892
|
+
selectedModel,
|
|
893
|
+
reviewsDir
|
|
894
|
+
};
|
|
895
|
+
}
|
|
896
|
+
async function findStoriesInEpic(storiesDir, epicNumber) {
|
|
897
|
+
if (!existsSync(storiesDir)) {
|
|
898
|
+
return [];
|
|
899
|
+
}
|
|
900
|
+
const files = await readdir(storiesDir);
|
|
901
|
+
const storyPattern = new RegExp(`^story-${epicNumber}-(\\d+)\\.md$`);
|
|
902
|
+
const stories = [];
|
|
903
|
+
for (const file of files) {
|
|
904
|
+
const match = file.match(storyPattern);
|
|
905
|
+
if (match) {
|
|
906
|
+
const storyNumber = match[1];
|
|
907
|
+
stories.push(`${epicNumber}.${storyNumber}`);
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
return stories.sort();
|
|
911
|
+
}
|
|
912
|
+
async function loadStoryFile2(storiesDir, storyId) {
|
|
913
|
+
const filename = `story-${storyId.replace(".", "-")}.md`;
|
|
914
|
+
const filePath = join(storiesDir, filename);
|
|
915
|
+
if (!existsSync(filePath)) {
|
|
916
|
+
return null;
|
|
917
|
+
}
|
|
918
|
+
return await readFile(filePath, "utf-8");
|
|
919
|
+
}
|
|
920
|
+
async function loadArchitecture(architectureFile) {
|
|
921
|
+
if (!existsSync(architectureFile)) {
|
|
922
|
+
return "";
|
|
923
|
+
}
|
|
924
|
+
return await readFile(architectureFile, "utf-8");
|
|
925
|
+
}
|
|
926
|
+
function normalizeStoryId(identifier) {
|
|
927
|
+
if (identifier.includes("/")) {
|
|
928
|
+
const filename = identifier.split("/").pop() || "";
|
|
929
|
+
const match = filename.match(/story-(\d+)-(\d+)\.md/);
|
|
930
|
+
if (match) {
|
|
931
|
+
return `${match[1]}.${match[2]}`;
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
return identifier.replace(/^story-/, "").replace("-", ".");
|
|
935
|
+
}
|
|
936
|
+
async function findExistingReviews(reviewsDir, storyId) {
|
|
937
|
+
if (!existsSync(reviewsDir)) {
|
|
938
|
+
return [];
|
|
939
|
+
}
|
|
940
|
+
const files = await readdir(reviewsDir);
|
|
941
|
+
const reviews = [];
|
|
942
|
+
const [epicNum] = storyId.split(".");
|
|
943
|
+
for (const file of files) {
|
|
944
|
+
if (file.startsWith(`party-review-epic-${epicNum}-`)) {
|
|
945
|
+
const filePath = join(reviewsDir, file);
|
|
946
|
+
const match = file.match(/(\d{4}-\d{2}-\d{2})/);
|
|
947
|
+
reviews.push({
|
|
948
|
+
type: "epic",
|
|
949
|
+
filePath,
|
|
950
|
+
date: match ? match[1] : "",
|
|
951
|
+
findingsCount: 0,
|
|
952
|
+
acceptedCount: 0,
|
|
953
|
+
deferredCount: 0,
|
|
954
|
+
rejectedCount: 0
|
|
955
|
+
});
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
return reviews;
|
|
959
|
+
}
|
|
960
|
+
async function analyzeStoryComplexity(storyContent) {
|
|
961
|
+
const acMatches = storyContent.match(/^- /gm) || [];
|
|
962
|
+
const acceptanceCriteriaCount = acMatches.length;
|
|
963
|
+
const securityKeywords = /\b(auth|login|password|token|secret|encrypt|permission|role|access)\b/i;
|
|
964
|
+
const hasSecurityConcerns = securityKeywords.test(storyContent);
|
|
965
|
+
const dataKeywords = /\b(database|schema|migration|model|table|collection)\b/i;
|
|
966
|
+
const hasDataModelChanges = dataKeywords.test(storyContent);
|
|
967
|
+
const apiKeywords = /\b(api|endpoint|route|controller|handler)\b/i;
|
|
968
|
+
const hasApiChanges = apiKeywords.test(storyContent);
|
|
969
|
+
const crudPattern = /\b(create|read|update|delete|get|post|put|patch|list)\b/gi;
|
|
970
|
+
const crudMatches = storyContent.match(crudPattern) || [];
|
|
971
|
+
const isCrudOnly = crudMatches.length > 0 && !hasSecurityConcerns && !hasDataModelChanges;
|
|
972
|
+
const isSimple = acceptanceCriteriaCount < 5 && !hasSecurityConcerns && !hasDataModelChanges && isCrudOnly;
|
|
973
|
+
return {
|
|
974
|
+
isSimple,
|
|
975
|
+
reason: isSimple ? `Simple story: ${acceptanceCriteriaCount} ACs, CRUD-only, no security/data concerns` : `Complex story: ${acceptanceCriteriaCount} ACs, security=${hasSecurityConcerns}, data=${hasDataModelChanges}, API=${hasApiChanges}`,
|
|
976
|
+
recommendedModel: isSimple ? "anthropic/claude-3-5-haiku-20241022" : "openai/gpt-5.2",
|
|
977
|
+
factors: {
|
|
978
|
+
acceptanceCriteriaCount,
|
|
979
|
+
hasSecurityConcerns,
|
|
980
|
+
hasDataModelChanges,
|
|
981
|
+
hasApiChanges,
|
|
982
|
+
isCrudOnly
|
|
983
|
+
}
|
|
984
|
+
};
|
|
985
|
+
}
|
|
986
|
+
function selectReviewModel(config, complexity) {
|
|
987
|
+
return complexity.isSimple ? "anthropic/claude-3-5-haiku-20241022" : config.models.oracle;
|
|
988
|
+
}
|
|
989
|
+
function buildEpicReviewPrompt(epicNumber, storyContents, architectureContent) {
|
|
990
|
+
const storiesText = storyContents.map((s) => `## Story ${s.id}
|
|
991
|
+
|
|
992
|
+
${s.content || "(empty)"}`).join("\n\n---\n\n");
|
|
993
|
+
return `You are a security, logic, and performance expert conducting a "party review" of BMAD stories BEFORE development begins.
|
|
994
|
+
|
|
995
|
+
**Your Role**: Find issues while they're cheap to fix (in markdown, not code).
|
|
996
|
+
|
|
997
|
+
**Focus Areas**:
|
|
998
|
+
1. \u{1F512} **Security Gaps**: Missing auth/authorization, input validation, data exposure risks, credential handling
|
|
999
|
+
2. \u{1F9E0} **Logic Gaps**: Edge cases not covered, error scenarios missing, validation rules incomplete, race conditions
|
|
1000
|
+
3. \u2728 **Best Practice Flaws**: Anti-patterns in requirements, testing strategy gaps, accessibility concerns, unclear specifications
|
|
1001
|
+
4. \u26A1 **Performance Issues**: Potential N+1 queries, missing caching strategy, large data handling not addressed, client-side bundle concerns
|
|
1002
|
+
|
|
1003
|
+
**Scope**: Epic ${epicNumber} - Review ALL stories for issues AND cross-story patterns
|
|
1004
|
+
|
|
1005
|
+
**Architecture Context**:
|
|
1006
|
+
${architectureContent || "(No architecture documented)"}
|
|
1007
|
+
|
|
1008
|
+
**Stories to Review**:
|
|
1009
|
+
${storiesText}
|
|
1010
|
+
|
|
1011
|
+
**Output Format** (JSON):
|
|
1012
|
+
{
|
|
1013
|
+
"summary": {
|
|
1014
|
+
"totalIssues": number,
|
|
1015
|
+
"highSeverity": number,
|
|
1016
|
+
"mediumSeverity": number,
|
|
1017
|
+
"lowSeverity": number,
|
|
1018
|
+
"recommendation": "string"
|
|
1019
|
+
},
|
|
1020
|
+
"storyFindings": [
|
|
1021
|
+
{
|
|
1022
|
+
"storyId": "string",
|
|
1023
|
+
"title": "string",
|
|
1024
|
+
"findings": {
|
|
1025
|
+
"security": [
|
|
1026
|
+
{
|
|
1027
|
+
"id": "unique-id",
|
|
1028
|
+
"severity": "high" | "medium" | "low",
|
|
1029
|
+
"title": "Brief title",
|
|
1030
|
+
"description": "What's wrong",
|
|
1031
|
+
"impact": "Why it matters",
|
|
1032
|
+
"suggestion": "How to fix"
|
|
1033
|
+
}
|
|
1034
|
+
],
|
|
1035
|
+
"logic": [...],
|
|
1036
|
+
"bestPractices": [...],
|
|
1037
|
+
"performance": [...]
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
],
|
|
1041
|
+
"crossStoryIssues": [
|
|
1042
|
+
{
|
|
1043
|
+
"id": "unique-id",
|
|
1044
|
+
"category": "security" | "logic" | "bestPractices" | "performance",
|
|
1045
|
+
"severity": "high" | "medium" | "low",
|
|
1046
|
+
"title": "Pattern or issue across multiple stories",
|
|
1047
|
+
"description": "Details",
|
|
1048
|
+
"affectedStories": ["2.1", "2.3"],
|
|
1049
|
+
"suggestion": "How to address"
|
|
1050
|
+
}
|
|
1051
|
+
]
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
**Instructions**:
|
|
1055
|
+
- Be thorough but practical
|
|
1056
|
+
- Prioritize high-impact issues
|
|
1057
|
+
- Provide actionable suggestions
|
|
1058
|
+
- Consider the architecture constraints
|
|
1059
|
+
- Flag missing requirements as logic gaps
|
|
1060
|
+
- Look for inconsistencies across stories`;
|
|
1061
|
+
}
|
|
1062
|
+
function buildFocusedReviewPrompt(storyId, storyContent, architectureContent, epicReview) {
|
|
1063
|
+
const epicContext = epicReview ? `
|
|
1064
|
+
|
|
1065
|
+
**Previous Epic Review**: An epic-level review was conducted on ${epicReview.date}. This focused review should find NEW issues not caught in the broader epic review.` : "";
|
|
1066
|
+
return `You are a security, logic, and performance expert conducting a DEEP DIVE "party review" of a single BMAD story BEFORE development begins.
|
|
1067
|
+
|
|
1068
|
+
**Your Role**: Find issues while they're cheap to fix (in markdown, not code). This is a FOCUSED review, so be more thorough than a broad epic review.
|
|
1069
|
+
|
|
1070
|
+
**Focus Areas**:
|
|
1071
|
+
1. \u{1F512} **Security Gaps**: Missing auth/authorization, input validation, data exposure risks, credential handling, session management
|
|
1072
|
+
2. \u{1F9E0} **Logic Gaps**: Edge cases not covered, error scenarios missing, validation rules incomplete, race conditions, state management
|
|
1073
|
+
3. \u2728 **Best Practice Flaws**: Anti-patterns in requirements, testing strategy gaps, accessibility concerns, unclear specifications, maintainability
|
|
1074
|
+
4. \u26A1 **Performance Issues**: Potential N+1 queries, missing caching strategy, large data handling, client-side bundle size, database indexes${epicContext}
|
|
1075
|
+
|
|
1076
|
+
**Story**: ${storyId}
|
|
1077
|
+
|
|
1078
|
+
**Architecture Context**:
|
|
1079
|
+
${architectureContent || "(No architecture documented)"}
|
|
1080
|
+
|
|
1081
|
+
**Story Content**:
|
|
1082
|
+
${storyContent}
|
|
1083
|
+
|
|
1084
|
+
**Output Format** (JSON):
|
|
1085
|
+
{
|
|
1086
|
+
"summary": {
|
|
1087
|
+
"totalIssues": number,
|
|
1088
|
+
"highSeverity": number,
|
|
1089
|
+
"mediumSeverity": number,
|
|
1090
|
+
"lowSeverity": number,
|
|
1091
|
+
"recommendation": "string"
|
|
1092
|
+
},
|
|
1093
|
+
"findings": {
|
|
1094
|
+
"security": [
|
|
1095
|
+
{
|
|
1096
|
+
"id": "unique-id",
|
|
1097
|
+
"severity": "high" | "medium" | "low",
|
|
1098
|
+
"title": "Brief title",
|
|
1099
|
+
"description": "What's wrong",
|
|
1100
|
+
"impact": "Why it matters",
|
|
1101
|
+
"suggestion": "How to fix (be specific)"
|
|
1102
|
+
}
|
|
1103
|
+
],
|
|
1104
|
+
"logic": [...],
|
|
1105
|
+
"bestPractices": [...],
|
|
1106
|
+
"performance": [...]
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
**Instructions**:
|
|
1111
|
+
- Be EXTREMELY thorough - this is a deep dive
|
|
1112
|
+
- Think like an adversarial tester trying to break the implementation
|
|
1113
|
+
- Question every assumption in the requirements
|
|
1114
|
+
- Look for vague or incomplete acceptance criteria
|
|
1115
|
+
- Consider security implications of every data flow
|
|
1116
|
+
- Flag missing error handling scenarios
|
|
1117
|
+
- Identify performance bottlenecks before they're coded
|
|
1118
|
+
- Provide highly specific, actionable suggestions`;
|
|
1119
|
+
}
|
|
767
1120
|
function createUpdateStatusTool(ctx, tracker, config) {
|
|
768
1121
|
return tool({
|
|
769
1122
|
description: `Update the BMAD sprint status for a story.
|
|
@@ -884,7 +1237,8 @@ function createTools(ctx, tracker, config) {
|
|
|
884
1237
|
athena_update_status: createUpdateStatusTool(ctx, tracker, config),
|
|
885
1238
|
athena_get_context: createGetContextTool(tracker),
|
|
886
1239
|
athena_parallel: createParallelTool(),
|
|
887
|
-
athena_config: createConfigTool(config)
|
|
1240
|
+
athena_config: createConfigTool(config),
|
|
1241
|
+
athena_review_story: createReviewStoryTool(ctx, config)
|
|
888
1242
|
};
|
|
889
1243
|
}
|
|
890
1244
|
var StoryTracker = class {
|