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/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 {