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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# dependencies (bun install)
node_modules

btca.config.jsonc

.claude/

Expand Down
2 changes: 1 addition & 1 deletion apps/cli/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "btca",
"author": "Ben Davis",
"version": "2.0.3",
"version": "2.0.4",
"homepage": "https://btca.dev",
"description": "CLI tool for asking questions about technologies using btca server",
"type": "module",
Expand Down
2 changes: 1 addition & 1 deletion apps/server/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "btca-server",
"version": "2.0.3",
"version": "2.0.4",
"description": "BTCA server for answering questions about your codebase using OpenCode AI",
"author": "Ben Davis",
"license": "MIT",
Expand Down
78 changes: 50 additions & 28 deletions apps/server/src/agent/loop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,35 +57,57 @@ export type AgentLoopResult = {
events: AgentEvent[];
};

const BASE_PROMPT = `
You are btca, an expert research agent. Your job is to answer questions from the user by searching the resources at your disposal.

<personality_and_writing_controls>
- Persona: an expert professional researcher
- Channel: internal
- Emotional register: direct, calm, and concise
- Formatting: bulleted/numbered lists are good + codeblocks
- Length: be thorough with your response, don't let it get too long though
- Default follow-through: don't ask permission to do the research, just do it and answer the question. ask for clarifications + suggest good follow up if needed
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo: "through" should be "thorough"

"be through with your response" is grammatically incorrect — it should be "be thorough with your response" (meaning detailed/complete).

Suggested change
- Default follow-through: don't ask permission to do the research, just do it and answer the question. ask for clarifications + suggest good follow up if needed
- Length: be thorough with your response, don't let it get too long though
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/server/src/agent/loop.ts
Line: 69

Comment:
Typo: "through" should be "thorough"

"be through with your response" is grammatically incorrect — it should be "be thorough with your response" (meaning detailed/complete).

```suggestion
- Length: be thorough with your response, don't let it get too long though
```

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

</personality_and_writing_controls>

<parallel_tool_calling>
- When multiple retrieval or lookup steps are independent, prefer parallel tool calls to reduce wall-clock time.
- Do not parallelize steps that have prerequisite dependencies or where one result determines the next action.
- After parallel retrieval, pause to synthesize the results before making more calls.
- Prefer selective parallelism: parallelize independent evidence gathering, not speculative or redundant tool use.
</parallel_tool_calling>

<tool_persistence_rules>
- Use tools whenever they materially improve correctness, completeness, or grounding.
- Do NOT stop early to save tool calls.
- Keep calling tools until either:
1) the task is complete
2) you've hit a doom loop where none of the tools function or something is missing
- If a tool returns empty/partial results, retry with a different strategy (query, filters, alternate source).
</tool_persistence_rules>

<completeness_contract>
- Treat the task as incomplete until you have a complete answer to the user's question that's grounded
- If any item is blocked by missing data, mark it [blocked] and state exactly what is missing.
</completeness_contract>

<dig_deeper_nudge>
- Don't stop at the first plausible answer.
- Look for second-order issues, edge cases, and missing constraints.
</dig_deeper_nudge>

<output_contract>
- Return a thorough answer to the user's question with real code examples
- Always output in proper markdown format
- Always include sources for your answer:
- For git resources, source links must be full github blob urls
- In "Sources", format git citations as markdown links: - [repo/relative/path.ext](https://github.com/.../blob/.../repo/relative/path.ext)".'
- For local resources cite local file paths
- For npm resources cite the path in the npm package
</output_contract>
`;

const buildSystemPrompt = (agentInstructions: string): string =>
[
'You are btca, an expert documentation search agent.',
'Your job is to answer questions by searching through the collection of resources.',
'',
'You have access to the following tools:',
'- read: Read file contents with line numbers',
'- grep: Search file contents using regex patterns',
'- glob: Find files matching glob patterns',
'- list: List directory contents',
'',
'Guidelines:',
'- Ground answers in the loaded resources. Do not rely on unstated prior knowledge.',
'- Search efficiently: start with one focused list/glob pass, then read likely files; only expand search when evidence is insufficient.',
'- Prefer targeted grep/read over broad repeated scans once candidate files are known.',
'- Give clear, unambiguous answers. State assumptions, prerequisites, and important version-sensitive caveats.',
'- For implementation/how-to questions, provide complete step-by-step instructions with commands and code snippets.',
'- Be concise but thorough in your responses.',
'- End every answer with a "Sources" section.',
'- For git resources, source links must be full GitHub blob URLs.',
'- In "Sources", format git citations as markdown links: "- [repo/relative/path.ext](https://github.com/.../blob/.../repo/relative/path.ext)".',
'- Do not use raw URLs as link labels.',
'- Do not repeat a URL in parentheses after a link.',
'- Do not output sources in "url (url)" format.',
'- For local resources, cite local file paths (no GitHub URL required).',
'- If you cannot find the answer, say so clearly',
'',
agentInstructions
].join('\n');
[BASE_PROMPT, agentInstructions].join('\n');

const createTools = (basePath: string, vfsId?: string) => ({
read: tool({
Expand Down
157 changes: 147 additions & 10 deletions apps/server/src/collections/service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,31 +4,45 @@ import os from 'node:os';
import path from 'node:path';

import type { ConfigService } from '../config/index.ts';
import type { ResourceDefinition } from '../resources/schema.ts';
import type { ResourcesService } from '../resources/service.ts';
import type { BtcaFsResource } from '../resources/types.ts';
import { createCollectionsService } from './service.ts';
import { disposeVirtualFs, existsInVirtualFs } from '../vfs/virtual-fs.ts';

const createLocalResource = (name: string, resourcePath: string) => ({
const createFsResource = ({
name,
resourcePath,
type = 'local',
repoSubPaths = [],
specialAgentInstructions = ''
}: {
name: string;
resourcePath: string;
type?: BtcaFsResource['type'];
repoSubPaths?: readonly string[];
specialAgentInstructions?: string;
}) => ({
_tag: 'fs-based' as const,
name,
fsName: name,
type: 'local' as const,
repoSubPaths: [],
specialAgentInstructions: '',
type,
repoSubPaths,
specialAgentInstructions,
getAbsoluteDirectoryPath: async () => resourcePath
});

const createConfigMock = () =>
const createConfigMock = (definitions: Record<string, ResourceDefinition> = {}) =>
({
getResource: () => undefined
getResource: (name: string) => definitions[name]
}) as unknown as ConfigService;

const createResourcesMock = (resourcePath: string) =>
const createResourcesMock = (loadPromise: ResourcesService['loadPromise']) =>
({
load: () => {
throw new Error('Not implemented in test');
},
loadPromise: async () => createLocalResource('repo', resourcePath)
loadPromise
}) as unknown as ResourcesService;

const runGit = (cwd: string, args: string[]) => {
Expand All @@ -55,7 +69,7 @@ describe('createCollectionsService', () => {
const resourcePath = await fs.mkdtemp(path.join(os.tmpdir(), 'btca-collections-git-'));
const collections = createCollectionsService({
config: createConfigMock(),
resources: createResourcesMock(resourcePath)
resources: createResourcesMock(async () => createFsResource({ name: 'repo', resourcePath }))
});

try {
Expand Down Expand Up @@ -89,7 +103,7 @@ describe('createCollectionsService', () => {
const resourcePath = await fs.mkdtemp(path.join(os.tmpdir(), 'btca-collections-local-'));
const collections = createCollectionsService({
config: createConfigMock(),
resources: createResourcesMock(resourcePath)
resources: createResourcesMock(async () => createFsResource({ name: 'repo', resourcePath }))
});

try {
Expand All @@ -109,6 +123,129 @@ describe('createCollectionsService', () => {
false
);
expect(await existsInVirtualFs('/repo/dist/bundle.js', collection.vfsId)).toBe(false);
expect(collection.agentInstructions).not.toContain('<special_notes>');
} finally {
await cleanupCollection(collection);
}
} finally {
await fs.rm(resourcePath, { recursive: true, force: true });
}
});

it('includes git citation metadata in agent instructions', async () => {
const resourcePath = await fs.mkdtemp(path.join(os.tmpdir(), 'btca-collections-git-meta-'));
const collections = createCollectionsService({
config: createConfigMock({
docs: {
type: 'git',
name: 'docs',
url: 'https://github.com/example/repo.git',
branch: 'main',
searchPath: 'guides',
specialNotes: 'Prefer the guides folder.'
}
}),
resources: createResourcesMock(async () =>
createFsResource({
name: 'docs',
resourcePath,
type: 'git',
repoSubPaths: ['guides'],
specialAgentInstructions: 'Prefer the guides folder.'
})
)
});

try {
await fs.writeFile(path.join(resourcePath, 'README.md'), 'hello\n');
runGit(resourcePath, ['init', '-q']);
runGit(resourcePath, ['config', 'user.email', 'test@example.com']);
runGit(resourcePath, ['config', 'user.name', 'BTCA Test']);
runGit(resourcePath, ['add', 'README.md']);
runGit(resourcePath, ['commit', '-m', 'init']);

const collection = await collections.loadPromise({ resourceNames: ['docs'] });

try {
expect(collection.agentInstructions).toContain(
'<repo_url>https://github.com/example/repo</repo_url>'
);
expect(collection.agentInstructions).toContain('<repo_branch>main</repo_branch>');
expect(collection.agentInstructions).toContain(
'<github_blob_prefix>https://github.com/example/repo/blob/main</github_blob_prefix>'
);
expect(collection.agentInstructions).toContain(
'<citation_rule>Convert virtual paths under ./docs/ to repo-relative paths, then encode each path segment for GitHub URLs.</citation_rule>'
);
expect(collection.agentInstructions).toContain('<path>./docs/guides</path>');
expect(collection.agentInstructions).toContain('<repo_commit>');
expect(collection.agentInstructions).toContain(
'<special_notes>Prefer the guides folder.</special_notes>'
);
} finally {
await cleanupCollection(collection);
}
} finally {
await fs.rm(resourcePath, { recursive: true, force: true });
}
});

it('includes npm citation metadata in agent instructions', async () => {
const resourcePath = await fs.mkdtemp(path.join(os.tmpdir(), 'btca-collections-npm-meta-'));
const collections = createCollectionsService({
config: createConfigMock({
react: {
type: 'npm',
name: 'react',
package: 'react',
version: '19.0.0',
specialNotes: 'Use package docs.'
}
}),
resources: createResourcesMock(async () =>
createFsResource({
name: 'react',
resourcePath,
type: 'npm',
specialAgentInstructions: 'Use package docs.'
})
)
});

try {
await fs.writeFile(
path.join(resourcePath, '.btca-npm-meta.json'),
JSON.stringify({
packageName: 'react',
resolvedVersion: '19.0.0',
packageUrl: 'https://www.npmjs.com/package/react'
})
);
await fs.writeFile(path.join(resourcePath, 'README.md'), 'react docs\n');

const collection = await collections.loadPromise({ resourceNames: ['react'] });

try {
expect(collection.agentInstructions).toContain('<npm_package>react</npm_package>');
expect(collection.agentInstructions).toContain('<npm_version>19.0.0</npm_version>');
expect(collection.agentInstructions).toContain(
'<npm_url>https://www.npmjs.com/package/react</npm_url>'
);
expect(collection.agentInstructions).toContain(
'<npm_citation_alias>npm:react@19.0.0</npm_citation_alias>'
);
expect(collection.agentInstructions).toContain(
'<npm_file_url_prefix>https://unpkg.com/react@19.0.0</npm_file_url_prefix>'
);
expect(collection.agentInstructions).toContain(
'<citation_rule>In Sources, cite npm files using npm:react@19.0.0/&lt;file&gt; and link them to https://unpkg.com/react@19.0.0/&lt;file&gt;. Do not cite encoded virtual folder names.</citation_rule>'
);
expect(collection.agentInstructions).toContain(
'<citation_example>https://unpkg.com/react@19.0.0/package.json</citation_example>'
);
expect(collection.agentInstructions).toContain(
'<special_notes>Use package docs.</special_notes>'
);
} finally {
await cleanupCollection(collection);
}
Expand Down
Loading
Loading