feat: Enhance issue management with batch processing and solution querying

- Updated issue loading process to create batches based on size (max 3 per batch).
- Removed semantic grouping in favor of simple size-based batching.
- Introduced a new command to batch query solutions for multiple issues.
- Improved solution loading to fetch all planned issues with bound solutions in a single call.
- Added detailed handling for multi-solution issues, including user selection for binding.
- Created a new workflow command for multi-agent development with documented progress and incremental iteration support.
- Added .gitignore for ace-tool directory to prevent unnecessary files from being tracked.
This commit is contained in:
catlog22
2026-01-23 16:55:10 +08:00
parent d6a3da2084
commit 54ec6a7c57
5 changed files with 1184 additions and 72 deletions

View File

@@ -59,7 +59,7 @@ Phase 1: Issue Loading
├─ Parse input (single, comma-separated, or --all-pending)
├─ Fetch issue metadata (ID, title, tags)
├─ Validate issues exist (create if needed)
└─ Group by similarity (shared tags or title keywords, max 3 per batch)
└─ Create batches by size (max 3 per batch by default)
Phase 2: Unified Explore + Plan (issue-plan-agent)
├─ Launch issue-plan-agent per batch
@@ -119,46 +119,17 @@ if (useAllPending) {
}
// Note: Agent fetches full issue content via `ccw issue status <id> --json`
// Semantic grouping via Gemini CLI (max 4 issues per group)
async function groupBySimilarityGemini(issues) {
const issueSummaries = issues.map(i => ({
id: i.id, title: i.title, tags: i.tags
}));
const prompt = `
PURPOSE: Group similar issues by semantic similarity for batch processing; maximize within-group coherence; max 4 issues per group
TASK: • Analyze issue titles/tags semantically • Identify functional/architectural clusters • Assign each issue to one group
MODE: analysis
CONTEXT: Issue metadata only
EXPECTED: JSON with groups array, each containing max 4 issue_ids, theme, rationale
CONSTRAINTS: Each issue in exactly one group | Max 4 issues per group | Balance group sizes
INPUT:
${JSON.stringify(issueSummaries, null, 2)}
OUTPUT FORMAT:
{"groups":[{"group_id":1,"theme":"...","issue_ids":["..."],"rationale":"..."}],"ungrouped":[]}
`;
const taskId = Bash({
command: `ccw cli -p "${prompt}" --tool gemini --mode analysis`,
run_in_background: true, timeout: 600000
});
const output = TaskOutput({ task_id: taskId, block: true });
// Extract JSON from potential markdown code blocks
function extractJsonFromMarkdown(text) {
const jsonMatch = text.match(/```json\s*\n([\s\S]*?)\n```/) ||
text.match(/```\s*\n([\s\S]*?)\n```/);
return jsonMatch ? jsonMatch[1] : text;
// Simple size-based batching (max batchSize issues per group)
function createBatches(issues, batchSize) {
const batches = [];
for (let i = 0; i < issues.length; i += batchSize) {
batches.push(issues.slice(i, i + batchSize));
}
const result = JSON.parse(extractJsonFromMarkdown(output));
return result.groups.map(g => g.issue_ids.map(id => issues.find(i => i.id === id)));
return batches;
}
const batches = await groupBySimilarityGemini(issues);
console.log(`Processing ${issues.length} issues in ${batches.length} batch(es) (max 4 issues/agent)`);
const batches = createBatches(issues, batchSize);
console.log(`Processing ${issues.length} issues in ${batches.length} batch(es) (max ${batchSize} issues/agent)`);
TodoWrite({
todos: batches.map((_, i) => ({
@@ -207,7 +178,9 @@ ${issueList}
- Add explicit verification steps to prevent same failure mode
6. **If github_url exists**: Add final task to comment on GitHub issue
7. Write solution to: .workflow/issues/solutions/{issue-id}.jsonl
8. Single solution → auto-bind; Multiple → return for selection
8. **CRITICAL - Binding Decision**:
- Single solution → **MUST execute**: ccw issue bind <issue-id> <solution-id>
- Multiple solutions → Return pending_selection only (no bind)
### Failure-Aware Planning Rules
- **Extract failure patterns**: Parse issue.feedback where type='failure' and stage='execute'
@@ -265,35 +238,55 @@ for (let i = 0; i < agentTasks.length; i += MAX_PARALLEL) {
}
agentResults.push(summary); // Store for Phase 3 conflict aggregation
// Verify binding for bound issues (agent should have executed bind)
for (const item of summary.bound || []) {
console.log(`${item.issue_id}: ${item.solution_id} (${item.task_count} tasks)`);
const status = JSON.parse(Bash(`ccw issue status ${item.issue_id} --json`).trim());
if (status.bound_solution_id === item.solution_id) {
console.log(`${item.issue_id}: ${item.solution_id} (${item.task_count} tasks)`);
} else {
// Fallback: agent failed to bind, execute here
Bash(`ccw issue bind ${item.issue_id} ${item.solution_id}`);
console.log(`${item.issue_id}: ${item.solution_id} (${item.task_count} tasks) [recovered]`);
}
}
// Collect and notify pending selections
// Collect pending selections for Phase 3
for (const pending of summary.pending_selection || []) {
console.log(`${pending.issue_id}: ${pending.solutions.length} solutions → awaiting selection`);
pendingSelections.push(pending);
}
if (summary.conflicts?.length > 0) {
console.log(`⚠ Conflicts: ${summary.conflicts.length} detected (will resolve in Phase 3)`);
}
updateTodo(`Plan batch ${batchIndex + 1}`, 'completed');
}
}
```
### Phase 3: Conflict Resolution & Solution Selection
### Phase 3: Solution Selection (if pending)
**Conflict Handling:**
- Collect `conflicts` from all agent results
- Low/Medium severity → auto-resolve with `recommended_resolution`
- High severity → use `AskUserQuestion` to let user choose resolution
```javascript
// Handle multi-solution issues
for (const pending of pendingSelections) {
if (pending.solutions.length === 0) continue;
**Multi-Solution Selection:**
- If `pending_selection` contains issues with multiple solutions:
- Use `AskUserQuestion` to present options (solution ID + task count + description)
- Extract selected solution ID from user response
- Verify solution file exists, recover from payload if missing
- Bind selected solution via `ccw issue bind <issue-id> <solution-id>`
const options = pending.solutions.slice(0, 4).map(sol => ({
label: `${sol.id} (${sol.task_count} tasks)`,
description: sol.description || sol.approach || 'No description'
}));
const answer = AskUserQuestion({
questions: [{
question: `Issue ${pending.issue_id}: which solution to bind?`,
header: pending.issue_id,
options: options,
multiSelect: false
}]
});
const selected = answer[Object.keys(answer)[0]];
if (!selected || selected === 'Other') continue;
const solId = selected.split(' ')[0];
Bash(`ccw issue bind ${pending.issue_id} ${solId}`);
console.log(`${pending.issue_id}: ${solId} bound`);
}
```
### Phase 4: Summary

View File

@@ -28,12 +28,13 @@ Queue formation command using **issue-queue-agent** that analyzes all bound solu
| Operation | Correct | Incorrect |
|-----------|---------|-----------|
| List issues (brief) | `ccw issue list --status planned --brief` | `Read('issues.jsonl')` |
| **Batch solutions (NEW)** | `ccw issue solutions --status planned --brief` | Loop `ccw issue solution <id>` |
| List queue (brief) | `ccw issue queue --brief` | `Read('queues/*.json')` |
| Read issue details | `ccw issue status <id> --json` | `Read('issues.jsonl')` |
| Get next item | `ccw issue next --json` | `Read('queues/*.json')` |
| Update status | `ccw issue update <id> --status ...` | Direct file edit |
| Sync from queue | `ccw issue update --from-queue` | Direct file edit |
| **Read solution (brief)** | `ccw issue solution <id> --brief` | `Read('solutions/*.jsonl')` |
| Read solution (single) | `ccw issue solution <id> --brief` | `Read('solutions/*.jsonl')` |
**Output Options**:
- `--brief`: JSON with minimal fields (id, status, counts)
@@ -131,24 +132,23 @@ Phase 7: Active Queue Check & Decision (REQUIRED)
### Phase 1: Solution Loading & Distribution
**Data Loading:**
- Use `ccw issue list --status planned --brief` to get planned issues with `bound_solution_id`
- If no planned issues found → display message, suggest `/issue:plan`
**Solution Brief Loading** (for each planned issue):
```bash
ccw issue solution <issue-id> --brief
# Returns: [{ solution_id, is_bound, task_count, files_touched[] }]
```
- Use `ccw issue solutions --status planned --brief` to get all planned issues with solutions in **one call**
- Returns: Array of `{ issue_id, solution_id, is_bound, task_count, files_touched[], priority }`
- If no bound solutions found → display message, suggest `/issue:plan`
**Build Solution Objects:**
```json
{
"issue_id": "ISS-xxx",
"solution_id": "SOL-ISS-xxx-1",
"task_count": 3,
"files_touched": ["src/auth.ts", "src/utils.ts"],
"priority": "medium"
```javascript
// Single CLI call replaces N individual queries
const result = Bash(`ccw issue solutions --status planned --brief`).trim();
const solutions = result ? JSON.parse(result) : [];
if (solutions.length === 0) {
console.log('No bound solutions found. Run /issue:plan first.');
return;
}
// solutions already in correct format:
// { issue_id, solution_id, is_bound, task_count, files_touched[], priority }
```
**Multi-Queue Distribution** (if `--queues > 1`):

File diff suppressed because it is too large Load Diff

View File

@@ -1220,6 +1220,81 @@ async function solutionAction(issueId: string | undefined, options: IssueOptions
}
}
/**
* solutions - Batch query solutions for multiple issues
* Usage: ccw issue solutions --status planned --brief
*/
async function solutionsAction(options: IssueOptions): Promise<void> {
// Get issues filtered by status
const issues = readIssues();
let targetIssues = issues;
if (options.status) {
const statuses = options.status.split(',').map((s: string) => s.trim());
targetIssues = issues.filter((i: Issue) => statuses.includes(i.status));
}
// Filter to only issues with bound_solution_id
const boundIssues = targetIssues.filter((i: Issue) => i.bound_solution_id);
if (boundIssues.length === 0) {
if (options.json || options.brief) {
console.log('[]');
} else {
console.log(chalk.yellow('No bound solutions found'));
}
return;
}
// Collect solutions for all bound issues
const allSolutions: Array<{
issue_id: string;
solution_id: string;
is_bound: boolean;
task_count: number;
files_touched: string[];
priority?: number;
}> = [];
for (const issue of boundIssues) {
const solutions = readSolutions(issue.id);
const boundSolution = solutions.find(s => s.id === issue.bound_solution_id);
if (boundSolution) {
const filesTouched = new Set<string>();
for (const task of boundSolution.tasks) {
if (task.modification_points) {
for (const mp of task.modification_points) {
if (mp.file) filesTouched.add(mp.file);
}
}
}
allSolutions.push({
issue_id: issue.id,
solution_id: boundSolution.id,
is_bound: true,
task_count: boundSolution.tasks.length,
files_touched: Array.from(filesTouched),
priority: issue.priority
});
}
}
// Brief mode: already minimal
if (options.brief || options.json) {
console.log(JSON.stringify(allSolutions, null, 2));
return;
}
// Human-readable output
console.log(chalk.bold.cyan(`\nBound Solutions (${allSolutions.length}):\n`));
for (const sol of allSolutions) {
console.log(`${chalk.green('◉')} ${sol.issue_id}${sol.solution_id}`);
console.log(chalk.gray(` Tasks: ${sol.task_count}, Files: ${sol.files_touched.length}`));
}
}
/**
* init - Initialize a new issue (manual ID)
*/
@@ -2832,6 +2907,9 @@ export async function issueCommand(
case 'solution':
await solutionAction(argsArray[0], options);
break;
case 'solutions':
await solutionsAction(options);
break;
case 'init':
await initAction(argsArray[0], options);
break;

1
ccw/src/core/routes/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
.ace-tool/