Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 44 additions & 12 deletions .cursor/commands/implement-story-tasks.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Implement Story Tasks (All Tasks in Sequence)

Orchestrates implementation of all tasks from a MegaBrain `*-tasks.md` file in order. For each task T1, T2, … that is not yet completed, runs a **task council** (three subagents in parallel) to produce an implementation brief, then runs the implement-task workflow via a subagent using that brief (without the full test suite per task), commits the result, then runs the next task. When all tasks are implemented, runs the full test suite once via a test-runner subagent to fix any failures.
Orchestrates implementation of all tasks from a MegaBrain `*-tasks.md` file in order. Creates a feature branch, then for each task T1, T2, … that is not yet completed, runs a **task council** (three subagents in parallel) to produce an implementation brief, then runs the implement-task workflow via a subagent using that brief (without the full test suite per task), commits the result, then runs the next task. When all tasks are implemented, runs the full test suite once via a test-runner subagent to fix any failures, then pushes the branch and creates a pull request with GitHub CLI (`gh`).

## Usage

Expand All @@ -24,22 +24,32 @@ Example:
- Build an ordered list of tasks that are **not** `Completed`: only these will be implemented in this run.
- If the list is empty, respond: "All tasks in this file are already completed." and stop.

2. **For each non-completed task in order (T1, then T2, …)**
2. **Create feature branch**
- Derive the **story slug** from the task file path: from the filename, strip `-tasks.md` (e.g. `US-03-02-openai-integration-tasks.md` → slug `US-03-02-openai-integration`). Branch name is `feature/<story-slug>`.
- If already on that branch, skip creation. Otherwise create and checkout:
- **Commands:**
```bash
# Create and switch to feature branch (derive story slug from task file, e.g. US-03-02-openai-integration)
git checkout -b feature/<story-slug>
```
- All subsequent commits go to this branch.

3. **For each non-completed task in order (T1, then T2, …)**
- Extract the **full task block** for that task (from the `### Tn:` heading through the end of that task section, i.e. until the next `### T...` or `---` / end of Task List).
- Derive the **story id** from the file (e.g. from "Story: US-03-02" in the file or from the filename like `US-03-02-openai-integration-tasks.md`).

- **2a. Task council** — Before implementing, run a council of three subagents in parallel to get recommendations. Invoke **three** `mcp_task` calls **in the same turn** (parallel), so the orchestrator waits for all three results before proceeding. Use the following:
- **3a. Task council** — Before implementing, run a council of three subagents in parallel to get recommendations. Invoke **three** `mcp_task` calls **in the same turn** (parallel), so the orchestrator waits for all three results before proceeding. Use the following:
- **Shared context** (include in each council prompt): story id, task id, full task block, task file path, path to related user story (same directory, `-tasks.md` replaced by `.md`), and the project's `docs/` folder as the canonical place for documentation updates.
- **Council 1 (best practices & implementation):** `subagent_type`: `code-reviewer` (or custom `council-best-practices` if available). **Prompt:** "You are acting as a task council member. Given the task below, recommend only: (1) best practices and patterns to follow for this codebase for this task, (2) best way to implement this task (interfaces, classes, error handling). Reply with a concise, actionable list. Do not implement code. Context: [story id], [task id], [full task block], [task file path], [related user story path], docs folder: docs/."
- **Council 2 (unit testing):** `subagent_type`: `generalPurpose` (or custom `council-testing` if available). **Prompt:** "You are acting as a task council member. Given the task below, recommend only: best way to unit test this task (test classes, scenarios, mocks, coverage focus). Reply with a concise, actionable list. Do not implement code. Context: [story id], [task id], [full task block], [task file path], [related user story path], docs folder: docs/."
- **Council 3 (documentation):** `subagent_type`: `doc-generator` (or custom `council-docs` if available). **Prompt:** "You are acting as a task council member. Given the task below, recommend only: what should be added or updated in the project's `docs/` folder (e.g. configuration-reference.md, getting-started.md, new or updated pages). Reply with a concise, actionable list. Do not edit files. Context: [story id], [task id], [full task block], [task file path], [related user story path], docs folder: docs/."

- **2b. Synthesis** — From the three council replies, the **coordinator** (orchestrator) produces a short **implementation brief**: a single bullet list merging (1) best practices to follow, (2) recommended implementation approach, (3) unit-test approach, (4) docs updates for `docs/`. Keep the brief concise and actionable.
- **3b. Synthesis** — From the three council replies, the **coordinator** (orchestrator) produces a short **implementation brief**: a single bullet list merging (1) best practices to follow, (2) recommended implementation approach, (3) unit-test approach, (4) docs updates for `docs/`. Keep the brief concise and actionable.

- **Invoke a subagent** to implement this single task:
- Use `mcp_task` with `subagent_type`: `generalPurpose`.
- **Prompt** (provide full context; subagents have no prior context):
- **Start with the council brief:** "Use the following council recommendations when implementing this task: [paste the implementation brief from 2b]. Then follow the implement-task process below."
- **Start with the council brief:** "Use the following council recommendations when implementing this task: [paste the implementation brief from 3b]. Then follow the implement-task process below."
- Instruct the subagent to implement **only** this task (the given task id and task block).
- Tell the subagent to follow the **implement-task** process in `.cursor/commands/implement-task.md` for the single task: task analysis, implementation planning, code, unit test authoring, and documentation/completion updates to the **tasks file** and the **related user story** file.
- **Important:** Tell the subagent: "You are running as part of implement-story-tasks. **Do not run the full test suite** (`mvn clean install`) at the end of this task. Run only `mvn compile` and, if needed, specific tests (e.g. `mvn test -Dtest=ClassName`) to catch obvious errors. Skip the 'MANDATORY (Final Validation): mvn clean install' and the Documentation & Completion gate that requires build success. Still update the task status to Completed and update the tasks file and related user story. The orchestrator will run the full test suite once at the end of the story."
Expand All @@ -55,12 +65,34 @@ Example:
- If the subagent failed or did not respond with "all done :)", report the failure and ask the user whether to continue to the next task or stop.
- After committing, **re-read the task file** to get the updated list of non-completed tasks for the next iteration (in case statuses changed). Or maintain the list and remove the current task; either way, process only non-completed tasks in order.

3. **When no tasks remain – run full test suite**
4. **When no tasks remain – run full test suite**
- When every task in the file has `**Status:** Completed`, run the full backend build and tests once, and fix any failures:
- Invoke a **test-runner** subagent via `mcp_task` with `subagent_type`: `test-runner`.
- **Prompt:** "Run the full backend build and test suite: `mvn clean install` in the backend directory. Fix any compilation or test failures. Report when the build succeeds or if you cannot fix the failures."
- Wait for the subagent to complete. If it reports success, optionally commit with a message like: `chore: verify story build – mvn clean install passes`. If it reports unresolved failures, report them to the user and do not commit.
- Then respond that the whole story tasks file has been implemented, list the tasks completed in this run, and confirm whether the final build passed.
- Then proceed to Step 5.

5. **Finalize and create pull request**
- **Prerequisite:** GitHub CLI (`gh`) must be installed and authenticated (`gh auth status`).
- **Commit if needed:** If there are uncommitted changes (`git status` shows changes), run:
```bash
git add .
git commit -m "chore: verify story build – mvn clean install passes"
```
- **Push the branch:**
```bash
git push -u origin feature/<story-slug>
```
- **Create pull request** (choose one):
```bash
gh pr create --title "feat: <story-id> <short-title>" --body "Implements all tasks from <task-file-name>. Resolves <story-id>."
```
or
```bash
gh pr create --fill
```
(`--fill` uses the first commit message as title/body.)
- Then respond that the whole story tasks file has been implemented, list the tasks completed in this run, confirm whether the final build passed, and give the PR URL (from `gh pr create` output).

## Task File and User Story Conventions

Expand All @@ -82,10 +114,10 @@ Custom council agents under `.cursor/agents/` can set different `model` values (

| Step | Action |
|------|--------|
| 1 | Parse task file → ordered list of non-completed tasks (T1, T2, …). |
| 2 | If list empty → "All tasks already completed." → stop. |
| 2a–2b | For each task: run **task council** (3 subagents in parallel: code-reviewer / council-best-practices, generalPurpose / council-testing, doc-generator / council-docs) → coordinator **synthesizes implementation brief** → pass brief to implement-task subagent. |
| 3 | For each task in list: run **generalPurpose** subagent (implement-task for that task only, **with council brief** in prompt; **skip full mvn clean install**; use mvn compile / targeted tests only) → on "all done :)", commit → next task. |
| 4 | When all tasks Completed → run **test-runner** subagent: `mvn clean install`, fix failures → optionally commit → report done. |
| 1 | Parse task file → ordered list of non-completed tasks (T1, T2, …). If list empty → "All tasks already completed." → stop. |
| 2 | Create branch `feature/<story-slug>` (`git checkout -b feature/<story-slug>`). |
| 3 | For each task: run **task council** (3a–3b), then **generalPurpose** subagent (implement-task with council brief; skip full `mvn clean install`; use mvn compile / targeted tests only) → on "all done :)", commit → next task. |
| 4 | When all tasks Completed → run **test-runner** subagent: `mvn clean install`, fix failures → optionally commit → then Step 5. |
| 5 | Commit any remaining changes (if needed) → `git push -u origin feature/<story-slug>` → `gh pr create` (with `--title`/`--body` or `--fill`) → report done and PR URL. |

Reference: [implement-task](.cursor/commands/implement-task.md) for the per-task process and subagent usage (explore, test-runner, verifier, etc.). The subagent should follow that command for the single task it is given.
7 changes: 7 additions & 0 deletions backend/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,13 @@
<artifactId>quarkus-picocli</artifactId>
</dependency>

<!-- Jansi for ANSI color codes (CLI syntax highlighting) -->
<dependency>
<groupId>org.fusesource.jansi</groupId>
<artifactId>jansi</artifactId>
<version>2.4.2</version>
</dependency>

<!-- Quartz for Scheduling -->
<dependency>
<groupId>io.quarkus</groupId>
Expand Down
87 changes: 1 addition & 86 deletions backend/src/main/java/io/megabrain/api/SearchResource.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import io.megabrain.core.FacetValue;
import io.megabrain.core.ResultMerger;
import io.megabrain.core.SearchFilters;

Check warning on line 10 in backend/src/main/java/io/megabrain/api/SearchResource.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this unused import 'io.megabrain.core.SearchFilters'.

See more on https://sonarcloud.io/project/issues?id=olexmal_MegaBrain&issues=AZzFj-W7ZwqQopZrMFQx&open=AZzFj-W7ZwqQopZrMFQx&pullRequest=28
import io.megabrain.core.SearchMode;
import io.megabrain.core.SearchOrchestrator;
import io.smallrye.mutiny.Uni;
Expand All @@ -23,8 +23,6 @@
import jakarta.ws.rs.core.Response;
import org.jboss.logging.Logger;

import org.apache.lucene.document.Document;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
Expand Down Expand Up @@ -103,7 +101,7 @@
* @return search response with results and pagination metadata
*/
@GET
public Uni<Response> search(

Check failure on line 104 in backend/src/main/java/io/megabrain/api/SearchResource.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this method to reduce its Cognitive Complexity from 19 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=olexmal_MegaBrain&issues=AZzFj-W7ZwqQopZrMFQt&open=AZzFj-W7ZwqQopZrMFQt&pullRequest=28
@QueryParam("q") String query,
@QueryParam("language") List<String> languages,
@QueryParam("repository") List<String> repositories,
Expand Down Expand Up @@ -151,7 +149,7 @@
searchRequest.setDepth(depth);

// Validate the request
try {

Check warning on line 152 in backend/src/main/java/io/megabrain/api/SearchResource.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Extract this nested try block into a separate method.

See more on https://sonarcloud.io/project/issues?id=olexmal_MegaBrain&issues=AZzFj-W7ZwqQopZrMFQs&open=AZzFj-W7ZwqQopZrMFQs&pullRequest=28
searchRequest.validate();
} catch (IllegalArgumentException e) {
LOG.warnf("Invalid search request: %s", e.getMessage());
Expand All @@ -171,7 +169,7 @@

// Resolve effective transitive depth (used only when transitive=true)
int effectiveDepth = searchRequest.getDepth() != null
? Math.max(1, Math.min(searchRequest.getDepth(), transitiveMaxDepth))

Check warning on line 172 in backend/src/main/java/io/megabrain/api/SearchResource.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use "Math.clamp" instead of "Math.min" or "Math.max".

See more on https://sonarcloud.io/project/issues?id=olexmal_MegaBrain&issues=AZzFj-W7ZwqQopZrMFQu&open=AZzFj-W7ZwqQopZrMFQu&pullRequest=28
: transitiveDefaultDepth;

// Parse search mode
Expand All @@ -189,8 +187,8 @@

// Convert merged results to SearchResult DTOs
List<SearchResult> results = mergedResults.stream()
.map(this::convertToSearchResult)
.map(SearchResultMapper::toSearchResult)
.collect(Collectors.toList());

Check warning on line 191 in backend/src/main/java/io/megabrain/api/SearchResource.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Replace this usage of 'Stream.collect(Collectors.toList())' with 'Stream.toList()' and ensure that the list is unmodified.

See more on https://sonarcloud.io/project/issues?id=olexmal_MegaBrain&issues=AZzFj-W7ZwqQopZrMFQv&open=AZzFj-W7ZwqQopZrMFQv&pullRequest=28

// Calculate pagination
int page = searchRequest.getOffset() / searchRequest.getLimit();
Expand Down Expand Up @@ -235,89 +233,6 @@
}
}

/**
* Converts a MergedResult to a SearchResult DTO.
*
* @param mergedResult the merged result from hybrid search
* @return SearchResult DTO
*/
private SearchResult convertToSearchResult(ResultMerger.MergedResult mergedResult) {
// Determine which result source to use (prefer Lucene if available)
String content;
String entityName;
String entityType;
String sourceFile;
String language;
String repository;
float score;
LineRange lineRange;

if (mergedResult.luceneDocument() != null) {
var luceneDoc = mergedResult.luceneDocument();
content = luceneDoc.get("content");
entityName = luceneDoc.get("entity_name");
entityType = luceneDoc.get("entity_type");
sourceFile = luceneDoc.get("source_file");
language = luceneDoc.get("language");
repository = luceneDoc.get("repository");
score = (float) mergedResult.combinedScore();
int startLine = getIntField(luceneDoc, "start_line", 1);
int endLine = getIntField(luceneDoc, "end_line", 1);
lineRange = new LineRange(startLine, endLine);
} else if (mergedResult.vectorResult() != null) {
var vectorMeta = mergedResult.vectorResult().metadata();
content = vectorMeta.content();
entityName = vectorMeta.entityName();
entityType = vectorMeta.entityType();
sourceFile = vectorMeta.sourceFile();
language = vectorMeta.language();
repository = ""; // Vector results don't have repository yet
score = (float) mergedResult.combinedScore();
lineRange = new LineRange(vectorMeta.startLine(), vectorMeta.endLine());
} else {
// Fallback (should not happen)
content = "";
entityName = "";
entityType = "";
sourceFile = "";
language = "";
repository = "";
score = 0.0f;
lineRange = new LineRange(1, 1);
}

FieldMatchInfo apiFieldMatch = mergedResult.fieldMatch() != null
? new FieldMatchInfo(mergedResult.fieldMatch().matchedFields(), mergedResult.fieldMatch().scores())
: null;
// Transitive metadata (US-02-06, T6): mark results from graph traversal and include relationship path
boolean isTransitive = mergedResult.transitivePath() != null;
List<String> relationshipPath = mergedResult.transitivePath() != null && !mergedResult.transitivePath().isEmpty()
? mergedResult.transitivePath()
: null;
return new SearchResult(content, entityName, entityType, sourceFile,
language, repository, score, lineRange, null, apiFieldMatch, isTransitive, relationshipPath);
}

/**
* Gets an integer field from a Lucene document, with a default value.
*
* @param doc the Lucene document
* @param fieldName the field name
* @param defaultValue the default value if field is missing or invalid
* @return the integer value
*/
private int getIntField(Document doc, String fieldName, int defaultValue) {
String value = doc.get(fieldName);
if (value == null || value.isBlank()) {
return defaultValue;
}
try {
return Integer.parseInt(value);
} catch (NumberFormatException e) {
return defaultValue;
}
}

/**
* Parses the search mode string to SearchMode enum.
*
Expand All @@ -330,7 +245,7 @@
}
try {
return SearchMode.valueOf(mode.toUpperCase());
} catch (IllegalArgumentException e) {

Check warning on line 248 in backend/src/main/java/io/megabrain/api/SearchResource.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Replace "e" with an unnamed pattern.

See more on https://sonarcloud.io/project/issues?id=olexmal_MegaBrain&issues=AZzFj-W7ZwqQopZrMFQw&open=AZzFj-W7ZwqQopZrMFQw&pullRequest=28
LOG.warnf("Invalid search mode '%s', defaulting to HYBRID", mode);
return SearchMode.HYBRID;
}
Expand Down
99 changes: 99 additions & 0 deletions backend/src/main/java/io/megabrain/api/SearchResultMapper.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/*
* Copyright (c) 2025 MegaBrain Contributors
* Licensed under the MIT License - see LICENSE file for details.
*/

package io.megabrain.api;

import io.megabrain.core.ResultMerger;
import io.megabrain.core.VectorStore;
import org.apache.lucene.document.Document;

import java.util.List;

/**
* Maps internal search result types to API DTOs.
* Shared by REST SearchResource and CLI SearchCommand.
*/
public final class SearchResultMapper {

private SearchResultMapper() {
}

/**
* Converts a MergedResult to a SearchResult DTO.
*
* @param mergedResult the merged result from hybrid search
* @return SearchResult DTO
*/
public static SearchResult toSearchResult(ResultMerger.MergedResult mergedResult) {

Check failure on line 29 in backend/src/main/java/io/megabrain/api/SearchResultMapper.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this method to reduce its Cognitive Complexity from 20 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=olexmal_MegaBrain&issues=AZzFj-YDZwqQopZrMFQy&open=AZzFj-YDZwqQopZrMFQy&pullRequest=28
String content;
String entityName;
String entityType;
String sourceFile;
String language;
String repository;
float score;
LineRange lineRange;

if (mergedResult.luceneDocument() != null) {
Document luceneDoc = mergedResult.luceneDocument();
content = luceneDoc.get("content");
entityName = luceneDoc.get("entity_name");
entityType = luceneDoc.get("entity_type");
sourceFile = luceneDoc.get("source_file");
language = luceneDoc.get("language");
repository = luceneDoc.get("repository");
score = (float) mergedResult.combinedScore();
int startLine = getIntField(luceneDoc, "start_line", 1);
int endLine = getIntField(luceneDoc, "end_line", 1);
lineRange = new LineRange(startLine, endLine);
} else if (mergedResult.vectorResult() != null) {
VectorStore.VectorMetadata vectorMeta = mergedResult.vectorResult().metadata();
content = vectorMeta != null ? vectorMeta.content() : "";
entityName = vectorMeta != null ? vectorMeta.entityName() : "";
entityType = vectorMeta != null ? vectorMeta.entityType() : "";
sourceFile = vectorMeta != null ? vectorMeta.sourceFile() : "";
language = vectorMeta != null ? vectorMeta.language() : "";
repository = "";
score = (float) mergedResult.combinedScore();
int start = vectorMeta != null ? vectorMeta.startLine() : 1;
int end = vectorMeta != null ? vectorMeta.endLine() : 1;
lineRange = new LineRange(start, end);
} else {
content = "";
entityName = "";
entityType = "";
sourceFile = "";
language = "";
repository = "";
score = 0.0f;
lineRange = new LineRange(1, 1);
}

FieldMatchInfo apiFieldMatch = mergedResult.fieldMatch() != null
? new FieldMatchInfo(mergedResult.fieldMatch().matchedFields(), mergedResult.fieldMatch().scores())
: null;
boolean isTransitive = mergedResult.transitivePath() != null;
List<String> relationshipPath = mergedResult.transitivePath() != null && !mergedResult.transitivePath().isEmpty()
? mergedResult.transitivePath()
: null;
return new SearchResult(content, entityName, entityType, sourceFile,
language, repository, score, lineRange, null, apiFieldMatch, isTransitive, relationshipPath);
}

/**
* Gets an integer field from a Lucene document, with a default value.
*/
public static int getIntField(Document doc, String fieldName, int defaultValue) {
String value = doc.get(fieldName);
if (value == null || value.isBlank()) {
return defaultValue;
}
try {
return Integer.parseInt(value);
} catch (NumberFormatException _) {
return defaultValue;
}
}
}
Loading