claude-all-hands 1.0.39 → 1.0.41
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/.claude/commands/address-pr-review.md +28 -0
- package/.claude/envoy/package-lock.json +7 -0
- package/.claude/envoy/package.json +1 -0
- package/.claude/envoy/src/commands/knowledge.ts +142 -12
- package/.claude/envoy/src/lib/agents/index.ts +54 -0
- package/.claude/envoy/src/lib/agents/prompts/knowledge-aggregator.md +67 -0
- package/.claude/envoy/src/lib/agents/runner.ts +251 -0
- package/.claude/envoy/src/lib/index.ts +10 -0
- package/.claude/settings.json +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Address PR review comments
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
<objective>
|
|
6
|
+
Process unresolved PR review comments for the current feature branch, prioritize them, and address each based on user direction.
|
|
7
|
+
</objective>
|
|
8
|
+
|
|
9
|
+
<process>
|
|
10
|
+
1. Read all unresolved review comments from the current feature branch's PR
|
|
11
|
+
2. Group any duplicative review feedback
|
|
12
|
+
3. Order concerns by priority using P1/P2/P3 indicators based on review indicators and your judgement
|
|
13
|
+
4. For EACH grouped concern, use AskUserQuestion to present it separately:
|
|
14
|
+
- Include P<N> priority indicator
|
|
15
|
+
- Describe the concern and proposed resolution action
|
|
16
|
+
- Offer multichoice options: approve, decline, or "say something" (provide specific instructions)
|
|
17
|
+
5. Execute on user's feedback for each concern
|
|
18
|
+
6. Commit and push changes with an inferred commit message
|
|
19
|
+
7. Resolve PR comments related to addressed issues (skip declined concerns)
|
|
20
|
+
8. If any comments remain unresolved, briefly summarize and ask user what to do next
|
|
21
|
+
</process>
|
|
22
|
+
|
|
23
|
+
<success_criteria>
|
|
24
|
+
- Each concern presented as separate AskUserQuestion with multichoice options
|
|
25
|
+
- Only approved/instructed concerns addressed
|
|
26
|
+
- Declined concerns left untouched in PR
|
|
27
|
+
- Changes committed and pushed
|
|
28
|
+
</success_criteria>
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"hasInstallScript": true,
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@google/genai": "^0.14.0",
|
|
13
|
+
"@opencode-ai/sdk": "^1.1.14",
|
|
13
14
|
"@upstash/context7-sdk": "^0.3.0",
|
|
14
15
|
"@visheratin/tokenizers-node": "0.1.5",
|
|
15
16
|
"@visheratin/web-ai-node": "^1.4.5",
|
|
@@ -498,6 +499,12 @@
|
|
|
498
499
|
"node": ">=18.0.0"
|
|
499
500
|
}
|
|
500
501
|
},
|
|
502
|
+
"node_modules/@opencode-ai/sdk": {
|
|
503
|
+
"version": "1.1.14",
|
|
504
|
+
"resolved": "https://registry.npmjs.org/@opencode-ai/sdk/-/sdk-1.1.14.tgz",
|
|
505
|
+
"integrity": "sha512-PJFu2QPxnOk0VZzlPm+IxhD1wSA41PJyCG6gkxAMI767gfAO96A0ukJJN7VK/gO6MbxLF5oTFaxBX5rAGcBRVw==",
|
|
506
|
+
"license": "MIT"
|
|
507
|
+
},
|
|
501
508
|
"node_modules/@pinojs/redact": {
|
|
502
509
|
"version": "0.4.0",
|
|
503
510
|
"resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz",
|
|
@@ -2,21 +2,39 @@
|
|
|
2
2
|
* Knowledge commands - semantic search and indexing for docs/ documentation.
|
|
3
3
|
*
|
|
4
4
|
* Commands:
|
|
5
|
-
* envoy knowledge search <query> [--metadata-only]
|
|
5
|
+
* envoy knowledge search <query> [--metadata-only] [--force-aggregate] [--no-aggregate]
|
|
6
6
|
* envoy knowledge reindex-all
|
|
7
7
|
* envoy knowledge reindex-from-changes [--files <json_array>]
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
import { readFileSync } from "fs";
|
|
11
|
+
import { dirname, join } from "path";
|
|
12
|
+
import { fileURLToPath } from "url";
|
|
10
13
|
import { Command } from "commander";
|
|
11
14
|
import { spawnSync } from "child_process";
|
|
12
15
|
import { BaseCommand, CommandResult } from "./base.js";
|
|
13
16
|
import { KnowledgeService, type FileChange } from "../lib/knowledge.js";
|
|
17
|
+
import {
|
|
18
|
+
AgentRunner,
|
|
19
|
+
type AggregatorOutput,
|
|
20
|
+
type SearchResult,
|
|
21
|
+
} from "../lib/agents/index.js";
|
|
14
22
|
import { getBaseBranch } from "../lib/git.js";
|
|
15
23
|
|
|
16
24
|
const getProjectRoot = (): string => {
|
|
17
25
|
return process.env.PROJECT_ROOT || process.cwd();
|
|
18
26
|
};
|
|
19
27
|
|
|
28
|
+
// Load aggregator prompt from file
|
|
29
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
30
|
+
const AGGREGATOR_PROMPT_PATH = join(__dirname, "../lib/agents/prompts/knowledge-aggregator.md");
|
|
31
|
+
|
|
32
|
+
const getAggregatorPrompt = (): string => {
|
|
33
|
+
return readFileSync(AGGREGATOR_PROMPT_PATH, "utf-8");
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const DEFAULT_TOKEN_THRESHOLD = 3500;
|
|
37
|
+
|
|
20
38
|
/**
|
|
21
39
|
* Auto-detect doc file changes since branch diverged from base.
|
|
22
40
|
* Returns FileChange[] for docs/ files only.
|
|
@@ -70,37 +88,119 @@ function getDocChangesFromGit(): FileChange[] {
|
|
|
70
88
|
}
|
|
71
89
|
|
|
72
90
|
/**
|
|
73
|
-
* Search command - semantic search against docs index
|
|
91
|
+
* Search command - semantic search against docs index with hybrid aggregation
|
|
74
92
|
*/
|
|
75
93
|
class SearchCommand extends BaseCommand {
|
|
76
94
|
readonly name = "search";
|
|
77
|
-
readonly description = "Semantic search docs (
|
|
95
|
+
readonly description = "Semantic search docs (aggregates large results automatically)";
|
|
78
96
|
|
|
79
97
|
defineArguments(cmd: Command): void {
|
|
80
98
|
cmd
|
|
81
99
|
.argument("<query>", "Descriptive phrase (e.g. 'how to handle API authentication' not 'auth')")
|
|
82
|
-
.option("--metadata-only", "Return only file paths and descriptions (no full content)")
|
|
100
|
+
.option("--metadata-only", "Return only file paths and descriptions (no full content)")
|
|
101
|
+
.option("--force-aggregate", "Force aggregation even below threshold")
|
|
102
|
+
.option("--no-aggregate", "Disable aggregation entirely");
|
|
83
103
|
}
|
|
84
104
|
|
|
85
105
|
async execute(args: Record<string, unknown>): Promise<CommandResult> {
|
|
86
106
|
const query = args.query as string;
|
|
87
107
|
const metadataOnly = !!args.metadataOnly;
|
|
108
|
+
const forceAggregate = !!args.forceAggregate;
|
|
109
|
+
const noAggregate = !!args.noAggregate;
|
|
88
110
|
|
|
89
111
|
if (!query) {
|
|
90
112
|
return this.error("validation_error", "query is required");
|
|
91
113
|
}
|
|
92
114
|
|
|
115
|
+
const projectRoot = getProjectRoot();
|
|
116
|
+
|
|
93
117
|
try {
|
|
94
|
-
const service = new KnowledgeService(
|
|
118
|
+
const service = new KnowledgeService(projectRoot);
|
|
95
119
|
const results = await service.search(query, 50, metadataOnly);
|
|
96
120
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
121
|
+
// Skip aggregation if metadata-only or explicitly disabled
|
|
122
|
+
if (metadataOnly || noAggregate) {
|
|
123
|
+
return this.success({
|
|
124
|
+
query,
|
|
125
|
+
metadata_only: metadataOnly,
|
|
126
|
+
results,
|
|
127
|
+
result_count: results.length,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Calculate total tokens
|
|
132
|
+
const totalTokens = results.reduce((sum, r) => sum + r.token_count, 0);
|
|
133
|
+
const parsedThreshold = parseInt(
|
|
134
|
+
process.env.KNOWLEDGE_AGGREGATOR_TOKEN_THRESHOLD ?? String(DEFAULT_TOKEN_THRESHOLD),
|
|
135
|
+
10
|
|
136
|
+
);
|
|
137
|
+
const threshold = Number.isNaN(parsedThreshold) ? DEFAULT_TOKEN_THRESHOLD : parsedThreshold;
|
|
138
|
+
|
|
139
|
+
// Skip aggregation if below threshold
|
|
140
|
+
if (totalTokens < threshold && !forceAggregate) {
|
|
141
|
+
return this.success({
|
|
142
|
+
aggregated: false,
|
|
143
|
+
total_tokens: totalTokens,
|
|
144
|
+
threshold,
|
|
145
|
+
results,
|
|
146
|
+
result_count: results.length,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Separate full vs minimized results
|
|
151
|
+
const fullResults = results.filter((r) => r.full_resource_context) as SearchResult[];
|
|
152
|
+
const minimizedResults = results
|
|
153
|
+
.filter((r) => !r.full_resource_context)
|
|
154
|
+
.map((r) => ({
|
|
155
|
+
resource_path: r.resource_path,
|
|
156
|
+
similarity: r.similarity,
|
|
157
|
+
token_count: r.token_count,
|
|
158
|
+
description: r.description,
|
|
159
|
+
relevant_files: r.relevant_files,
|
|
160
|
+
})) as SearchResult[];
|
|
161
|
+
|
|
162
|
+
// Run aggregator agent
|
|
163
|
+
try {
|
|
164
|
+
const runner = new AgentRunner(projectRoot);
|
|
165
|
+
const input = this.formatAggregatorInput(query, fullResults, minimizedResults);
|
|
166
|
+
|
|
167
|
+
const result = await runner.run<AggregatorOutput>(
|
|
168
|
+
{
|
|
169
|
+
name: "knowledge-aggregator",
|
|
170
|
+
systemPrompt: getAggregatorPrompt(),
|
|
171
|
+
timeoutMs: 60000,
|
|
172
|
+
},
|
|
173
|
+
input
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
if (!result.success || !result.data) {
|
|
177
|
+
// Fallback to raw results on aggregation failure
|
|
178
|
+
return this.success({
|
|
179
|
+
aggregated: false,
|
|
180
|
+
aggregation_error: result.error ?? "Unknown aggregation error",
|
|
181
|
+
total_tokens: totalTokens,
|
|
182
|
+
results,
|
|
183
|
+
result_count: results.length,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return this.success({
|
|
188
|
+
aggregated: true,
|
|
189
|
+
insight: result.data.insight,
|
|
190
|
+
references: result.data.references,
|
|
191
|
+
design_notes: result.data.design_notes,
|
|
192
|
+
metadata: result.metadata,
|
|
193
|
+
});
|
|
194
|
+
} catch (e) {
|
|
195
|
+
// Fallback to raw results on any error
|
|
196
|
+
return this.success({
|
|
197
|
+
aggregated: false,
|
|
198
|
+
aggregation_error: e instanceof Error ? e.message : String(e),
|
|
199
|
+
total_tokens: totalTokens,
|
|
200
|
+
results,
|
|
201
|
+
result_count: results.length,
|
|
202
|
+
});
|
|
203
|
+
}
|
|
104
204
|
} catch (e) {
|
|
105
205
|
return this.error(
|
|
106
206
|
"search_error",
|
|
@@ -108,6 +208,36 @@ class SearchCommand extends BaseCommand {
|
|
|
108
208
|
);
|
|
109
209
|
}
|
|
110
210
|
}
|
|
211
|
+
|
|
212
|
+
private formatAggregatorInput(
|
|
213
|
+
query: string,
|
|
214
|
+
fullResults: SearchResult[],
|
|
215
|
+
minimizedResults: SearchResult[]
|
|
216
|
+
): string {
|
|
217
|
+
return `## Query
|
|
218
|
+
${query}
|
|
219
|
+
|
|
220
|
+
## Full Results (${fullResults.length} documents with complete content)
|
|
221
|
+
|
|
222
|
+
${fullResults.map((r) => `### ${r.resource_path}
|
|
223
|
+
- Similarity: ${r.similarity.toFixed(3)}
|
|
224
|
+
- Tokens: ${r.token_count}
|
|
225
|
+
- Description: ${r.description}
|
|
226
|
+
|
|
227
|
+
Content:
|
|
228
|
+
\`\`\`
|
|
229
|
+
${r.full_resource_context}
|
|
230
|
+
\`\`\`
|
|
231
|
+
`).join("\n")}
|
|
232
|
+
|
|
233
|
+
## Minimized Results (${minimizedResults.length} documents - request expansion if needed)
|
|
234
|
+
|
|
235
|
+
${minimizedResults.map((r) => `- **${r.resource_path}** (similarity: ${r.similarity.toFixed(3)}, ${r.token_count} tokens)
|
|
236
|
+
${r.description}
|
|
237
|
+
`).join("\n")}
|
|
238
|
+
|
|
239
|
+
Please analyze and provide your response as JSON.`;
|
|
240
|
+
}
|
|
111
241
|
}
|
|
112
242
|
|
|
113
243
|
/**
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sub-agent infrastructure for envoy.
|
|
3
|
+
* Uses OpenCode SDK to spawn agents for specialized tasks.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Agent configuration
|
|
7
|
+
export interface AgentConfig {
|
|
8
|
+
name: string;
|
|
9
|
+
systemPrompt: string;
|
|
10
|
+
model?: string;
|
|
11
|
+
timeoutMs?: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Agent execution result
|
|
15
|
+
export interface AgentResult<T = unknown> {
|
|
16
|
+
success: boolean;
|
|
17
|
+
data?: T;
|
|
18
|
+
error?: string;
|
|
19
|
+
metadata?: {
|
|
20
|
+
model: string;
|
|
21
|
+
tokens_used?: number;
|
|
22
|
+
duration_ms: number;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Search result type (mirrors KnowledgeService.SearchResult)
|
|
27
|
+
export interface SearchResult {
|
|
28
|
+
resource_path: string;
|
|
29
|
+
similarity: number;
|
|
30
|
+
token_count: number;
|
|
31
|
+
description: string;
|
|
32
|
+
relevant_files: string[];
|
|
33
|
+
full_resource_context?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Knowledge aggregator input
|
|
37
|
+
export interface AggregatorInput {
|
|
38
|
+
query: string;
|
|
39
|
+
full_results: SearchResult[];
|
|
40
|
+
minimized_results: SearchResult[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Knowledge aggregator output
|
|
44
|
+
export interface AggregatorOutput {
|
|
45
|
+
insight: string;
|
|
46
|
+
references: Array<{
|
|
47
|
+
file: string;
|
|
48
|
+
symbol: string | null;
|
|
49
|
+
why: string;
|
|
50
|
+
}>;
|
|
51
|
+
design_notes?: string[];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export { AgentRunner } from "./runner.js";
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Knowledge Aggregator
|
|
2
|
+
|
|
3
|
+
You synthesize documentation into codebase-grounded answers. The caller is exploring a codebase - they need to understand **what exists**, **why it was built that way**, and **where to look**.
|
|
4
|
+
|
|
5
|
+
## Core Principle
|
|
6
|
+
|
|
7
|
+
Never answer generically. Every insight must reference concrete codebase implementations:
|
|
8
|
+
- BAD: "An agent is a specialized execution context..."
|
|
9
|
+
- GOOD: "Agents in this codebase are defined in `.claude/agents/` with configs that scope their tools - see `curator.md` for the pattern of restricting WebSearch to specific agent types"
|
|
10
|
+
|
|
11
|
+
## Input Format
|
|
12
|
+
|
|
13
|
+
You receive:
|
|
14
|
+
1. A user query about the codebase
|
|
15
|
+
2. Full results: Docs with complete content (high similarity)
|
|
16
|
+
3. Minimized results: Docs with metadata only (may need expansion)
|
|
17
|
+
|
|
18
|
+
## Expansion Protocol
|
|
19
|
+
|
|
20
|
+
Need content from a minimized result? Output:
|
|
21
|
+
```
|
|
22
|
+
EXPAND: <resource_path>
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
You'll receive the content. Max 3 expansions. Only expand if description suggests direct relevance.
|
|
26
|
+
|
|
27
|
+
## Output Format
|
|
28
|
+
|
|
29
|
+
Return ONLY valid JSON:
|
|
30
|
+
|
|
31
|
+
```json
|
|
32
|
+
{
|
|
33
|
+
"insight": "Codebase-grounded answer: what pattern exists, why it was chosen, how it's used. Include best practices if query implies implementation intent. 2-4 sentences max.",
|
|
34
|
+
"references": [
|
|
35
|
+
{
|
|
36
|
+
"file": "path/to/implementation.ts",
|
|
37
|
+
"symbol": "functionOrClassName",
|
|
38
|
+
"why": "Brief reason main agent should check this"
|
|
39
|
+
}
|
|
40
|
+
],
|
|
41
|
+
"design_notes": ["Relevant architectural decisions or tradeoffs from docs"]
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Field Guidelines
|
|
46
|
+
|
|
47
|
+
**insight**:
|
|
48
|
+
- Ground every statement in the codebase
|
|
49
|
+
- If query implies they want to implement something, include the recommended approach
|
|
50
|
+
- Mention specific files/patterns by name
|
|
51
|
+
- Include "best practice: X" when docs encode conventions
|
|
52
|
+
|
|
53
|
+
**references** (max 5, ranked by relevance):
|
|
54
|
+
- `file`: Path to code file (from doc's `relevant_files` or inline references like `[ref:path:symbol:hash]`)
|
|
55
|
+
- `symbol`: Function/class/variable name if known from doc references, null otherwise
|
|
56
|
+
- `why`: One sentence - why should they look here? What will they find?
|
|
57
|
+
|
|
58
|
+
**design_notes** (optional, max 2):
|
|
59
|
+
- Only include if docs explicitly discuss design rationale
|
|
60
|
+
- Format: "[Decision]: [Rationale]" e.g. "Least-privilege tooling: agents receive only tools for their function to prevent cross-domain actions"
|
|
61
|
+
|
|
62
|
+
## Anti-patterns
|
|
63
|
+
|
|
64
|
+
- Generic definitions not tied to this codebase
|
|
65
|
+
- Listing every file mentioned (keep only most relevant)
|
|
66
|
+
- Excerpts without actionability
|
|
67
|
+
- Restating the query as the answer
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgentRunner - Spawns and manages sub-agents via OpenCode SDK.
|
|
3
|
+
* Each run spawns a fresh server instance, executes the agent, and cleans up.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { existsSync, readFileSync, statSync } from "fs";
|
|
7
|
+
import { join } from "path";
|
|
8
|
+
import { createOpencode } from "@opencode-ai/sdk";
|
|
9
|
+
import type { AgentConfig, AgentResult } from "./index.js";
|
|
10
|
+
import { logCommandComplete, logCommandStart } from "../observability.js";
|
|
11
|
+
|
|
12
|
+
const MAX_EXPANSIONS = 3;
|
|
13
|
+
const EXPANSION_PATTERN = /^EXPAND:\s*(.+)$/gm;
|
|
14
|
+
const DEFAULT_TIMEOUT_MS = 60000;
|
|
15
|
+
|
|
16
|
+
export class AgentRunner {
|
|
17
|
+
private readonly projectRoot: string;
|
|
18
|
+
|
|
19
|
+
constructor(projectRoot: string) {
|
|
20
|
+
this.projectRoot = projectRoot;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Execute an agent with given config and user message.
|
|
25
|
+
* Spawns server, creates session, sends message, handles expansions, cleans up.
|
|
26
|
+
*/
|
|
27
|
+
async run<T>(config: AgentConfig, userMessage: string): Promise<AgentResult<T>> {
|
|
28
|
+
const startTime = Date.now();
|
|
29
|
+
logCommandStart("agent.run", { agent: config.name });
|
|
30
|
+
|
|
31
|
+
let server: { close: () => void } | null = null;
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const { client, server: srv } = await createOpencode({
|
|
35
|
+
timeout: config.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
36
|
+
config: config.model ? { model: config.model } : undefined,
|
|
37
|
+
});
|
|
38
|
+
server = srv;
|
|
39
|
+
|
|
40
|
+
const session = await client.session.create({
|
|
41
|
+
body: { title: config.name },
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
if (!session.data?.id) {
|
|
45
|
+
throw new Error("Failed to create session");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Send system prompt as context
|
|
49
|
+
await client.session.prompt({
|
|
50
|
+
path: { id: session.data.id },
|
|
51
|
+
body: {
|
|
52
|
+
noReply: true,
|
|
53
|
+
parts: [{ type: "text", text: config.systemPrompt }],
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// Send user message and get response
|
|
58
|
+
let response = await client.session.prompt({
|
|
59
|
+
path: { id: session.data.id },
|
|
60
|
+
body: {
|
|
61
|
+
parts: [{ type: "text", text: userMessage }],
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
let responseText = this.extractResponseText(response);
|
|
66
|
+
let expansionCount = 0;
|
|
67
|
+
|
|
68
|
+
// Handle expansion requests
|
|
69
|
+
while (this.hasExpansionRequest(responseText) && expansionCount < MAX_EXPANSIONS) {
|
|
70
|
+
const paths = this.extractExpansionPaths(responseText);
|
|
71
|
+
const contents = this.readFiles(paths);
|
|
72
|
+
const expansionMessage = this.formatExpansionResponse(contents);
|
|
73
|
+
|
|
74
|
+
response = await client.session.prompt({
|
|
75
|
+
path: { id: session.data.id },
|
|
76
|
+
body: {
|
|
77
|
+
parts: [{ type: "text", text: expansionMessage }],
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
responseText = this.extractResponseText(response);
|
|
82
|
+
expansionCount++;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const durationMs = Date.now() - startTime;
|
|
86
|
+
const parsed = this.parseStructuredOutput<T>(responseText);
|
|
87
|
+
|
|
88
|
+
if (!parsed) {
|
|
89
|
+
// Retry once asking for JSON
|
|
90
|
+
response = await client.session.prompt({
|
|
91
|
+
path: { id: session.data.id },
|
|
92
|
+
body: {
|
|
93
|
+
parts: [{ type: "text", text: "Please format your response as valid JSON matching the required schema." }],
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
responseText = this.extractResponseText(response);
|
|
98
|
+
const retryParsed = this.parseStructuredOutput<T>(responseText);
|
|
99
|
+
|
|
100
|
+
if (!retryParsed) {
|
|
101
|
+
throw new Error("Failed to parse agent response as JSON");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
logCommandComplete("agent.run", "success", Date.now() - startTime, {
|
|
105
|
+
agent: config.name,
|
|
106
|
+
expansions: expansionCount,
|
|
107
|
+
retry: true,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
success: true,
|
|
112
|
+
data: retryParsed,
|
|
113
|
+
metadata: {
|
|
114
|
+
model: config.model ?? "default",
|
|
115
|
+
duration_ms: Date.now() - startTime,
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
logCommandComplete("agent.run", "success", durationMs, {
|
|
121
|
+
agent: config.name,
|
|
122
|
+
expansions: expansionCount,
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
success: true,
|
|
127
|
+
data: parsed,
|
|
128
|
+
metadata: {
|
|
129
|
+
model: config.model ?? "default",
|
|
130
|
+
duration_ms: durationMs,
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
} catch (error) {
|
|
134
|
+
const durationMs = Date.now() - startTime;
|
|
135
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
136
|
+
|
|
137
|
+
logCommandComplete("agent.run", "error", durationMs, {
|
|
138
|
+
agent: config.name,
|
|
139
|
+
error: errorMessage,
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
success: false,
|
|
144
|
+
error: errorMessage,
|
|
145
|
+
metadata: {
|
|
146
|
+
model: config.model ?? "default",
|
|
147
|
+
duration_ms: durationMs,
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
} finally {
|
|
151
|
+
if (server) {
|
|
152
|
+
server.close();
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
private extractResponseText(response: unknown): string {
|
|
158
|
+
// Navigate SDK response structure to get text content
|
|
159
|
+
const resp = response as {
|
|
160
|
+
data?: {
|
|
161
|
+
parts?: Array<{ type: string; text?: string }>;
|
|
162
|
+
};
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
if (!resp.data?.parts) {
|
|
166
|
+
return "";
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return resp.data.parts
|
|
170
|
+
.filter((p) => p.type === "text" && p.text)
|
|
171
|
+
.map((p) => p.text)
|
|
172
|
+
.join("\n");
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
private hasExpansionRequest(text: string): boolean {
|
|
176
|
+
const regex = new RegExp(EXPANSION_PATTERN.source, EXPANSION_PATTERN.flags);
|
|
177
|
+
return regex.test(text);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
private extractExpansionPaths(text: string): string[] {
|
|
181
|
+
const paths: string[] = [];
|
|
182
|
+
const regex = new RegExp(EXPANSION_PATTERN.source, "gm");
|
|
183
|
+
let match;
|
|
184
|
+
|
|
185
|
+
while ((match = regex.exec(text)) !== null) {
|
|
186
|
+
paths.push(match[1].trim());
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return paths;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
private readFiles(paths: string[]): Map<string, string | null> {
|
|
193
|
+
const contents = new Map<string, string | null>();
|
|
194
|
+
const MAX_FILE_SIZE = 1024 * 1024; // 1MB
|
|
195
|
+
|
|
196
|
+
for (const path of paths) {
|
|
197
|
+
const fullPath = join(this.projectRoot, path);
|
|
198
|
+
if (existsSync(fullPath)) {
|
|
199
|
+
try {
|
|
200
|
+
const stats = statSync(fullPath);
|
|
201
|
+
if (stats.size > MAX_FILE_SIZE) {
|
|
202
|
+
const partial = readFileSync(fullPath, "utf-8").slice(0, MAX_FILE_SIZE);
|
|
203
|
+
contents.set(path, `${partial}\n\n[TRUNCATED: file exceeds 1MB limit]`);
|
|
204
|
+
} else {
|
|
205
|
+
contents.set(path, readFileSync(fullPath, "utf-8"));
|
|
206
|
+
}
|
|
207
|
+
} catch {
|
|
208
|
+
contents.set(path, null);
|
|
209
|
+
}
|
|
210
|
+
} else {
|
|
211
|
+
contents.set(path, null);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return contents;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
private formatExpansionResponse(contents: Map<string, string | null>): string {
|
|
219
|
+
const parts: string[] = ["Here are the expanded file contents:\n"];
|
|
220
|
+
|
|
221
|
+
for (const [path, content] of contents) {
|
|
222
|
+
if (content !== null) {
|
|
223
|
+
parts.push(`## ${path}\n\`\`\`\n${content}\n\`\`\`\n`);
|
|
224
|
+
} else {
|
|
225
|
+
parts.push(`## ${path}\n[File not found or unreadable]\n`);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
parts.push("\nPlease continue your analysis and provide the final JSON response.");
|
|
230
|
+
return parts.join("\n");
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
private parseStructuredOutput<T>(text: string): T | null {
|
|
234
|
+
// Try to extract JSON from code block
|
|
235
|
+
const codeBlockMatch = text.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
236
|
+
if (codeBlockMatch) {
|
|
237
|
+
try {
|
|
238
|
+
return JSON.parse(codeBlockMatch[1].trim()) as T;
|
|
239
|
+
} catch {
|
|
240
|
+
// Fall through to try raw parse
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Try raw JSON parse
|
|
245
|
+
try {
|
|
246
|
+
return JSON.parse(text.trim()) as T;
|
|
247
|
+
} catch {
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
@@ -114,3 +114,13 @@ export {
|
|
|
114
114
|
} from "./notification.js";
|
|
115
115
|
export type { NotifyOptions } from "./notification.js";
|
|
116
116
|
|
|
117
|
+
// Agent infrastructure
|
|
118
|
+
export { AgentRunner } from "./agents/index.js";
|
|
119
|
+
export type {
|
|
120
|
+
AgentConfig,
|
|
121
|
+
AgentResult,
|
|
122
|
+
AggregatorInput,
|
|
123
|
+
AggregatorOutput,
|
|
124
|
+
SearchResult,
|
|
125
|
+
} from "./agents/index.js";
|
|
126
|
+
|
package/.claude/settings.json
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
"SEARCH_SIMILARITY_THRESHOLD": "0.65",
|
|
6
6
|
"SEARCH_FULL_CONTEXT_SIMILARITY_THRESHOLD": "0.82",
|
|
7
7
|
"SEARCH_CONTEXT_TOKEN_LIMIT": "5000",
|
|
8
|
+
"KNOWLEDGE_AGGREGATOR_TOKEN_THRESHOLD": "3500",
|
|
8
9
|
"MAX_LOGS_TOKENS": "10000",
|
|
9
10
|
"BASH_MAX_TIMEOUT_MS": "3600000",
|
|
10
11
|
"BASE_BRANCH": "main",
|