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