Skip to content

Commit be0ffc6

Browse files
Sync public snapshot from freebuff-private
Source: CodebuffAI/freebuff-private@8b332c393245071f740883d327b697918f50aa42
1 parent b2ae023 commit be0ffc6

10 files changed

Lines changed: 1204 additions & 2 deletions

File tree

‎common/src/__tests__/free-agents.test.ts‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import {
3030
FREEBUFF_DESKTOP_AUTORUN_AGENT_ID,
3131
FREEBUFF_DESKTOP_THREAD_AGENT_IDS,
3232
FREEBUFF_REVIEWER_AGENT_ID_BY_MODEL,
33+
FREEBUFF_WORKSPACE_PROFILER_AGENT_ID,
3334
FREEBUFF_WEB_BASE3_AGENT_ID_BY_MODEL,
3435
FREE_MODE_AGENT_MODELS,
3536
FREE_TIER_AGENT_MODELS,
@@ -799,6 +800,8 @@ describe('every freebuff root agent declares a prompt opening', () => {
799800
const CLOUD_PLANNER = 'You are Buffy, the Freebuff Cloud project planner.'
800801
const DESKTOP_AUTORUN =
801802
'You are Buffy, the auto-run agent behind Freebuff Desktop.'
803+
const WORKSPACE_PROFILER =
804+
'You are Buffy, the workspace profiler behind Freebuff.'
802805

803806
/** Root agent id → the opening its system prompt starts with. */
804807
const PROMPT_FAMILY: Record<string, string> = {
@@ -897,6 +900,9 @@ describe('every freebuff root agent declares a prompt opening', () => {
897900
// onto base3's: base3 tells the model it is the coding agent, and this one
898901
// exists to say it is not.
899902
[FREEBUFF_DESKTOP_AUTORUN_AGENT_ID]: DESKTOP_AUTORUN,
903+
// The workspace profiler (common/src/workspace-profile/agent.ts) is not a
904+
// coding agent either, and says so first.
905+
[FREEBUFF_WORKSPACE_PROFILER_AGENT_ID]: WORKSPACE_PROFILER,
900906
}
901907

902908
test('no root agent is missing from the prompt-family map', () => {

‎common/src/constants/free-agents.ts‎

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,24 @@ export const FREEBUFF_DESKTOP_THREAD_AGENT_ID = 'freebuff-desktop-thread'
116116
*/
117117
export const FREEBUFF_DESKTOP_AUTORUN_AGENT_ID = 'freebuff-desktop-autorun'
118118

119+
/**
120+
* The root the workspace profiler runs under: a read-only agent that looks over
121+
* the project a Desktop session is working in and fills in the
122+
* backend's workspace-profile template (common/src/workspace-profile/). It
123+
* never edits files or runs commands.
124+
*
125+
* A first-party free-mode ROOT for the auto-run decider's reason: it runs
126+
* beside a turn, not inside one, so there is no running root for it to hang
127+
* off and the subagent hierarchy gate would 403 it. Unlike the decider it is
128+
* pinned to one cheap model, whatever the session's model is: see
129+
* isFreebuffHelperOnSessionModel.
130+
*/
131+
export const FREEBUFF_WORKSPACE_PROFILER_AGENT_ID = 'freebuff-workspace-profiler'
132+
133+
/** The one model the workspace profiler runs, on every surface. */
134+
export const FREEBUFF_WORKSPACE_PROFILER_MODEL_ID =
135+
FREEBUFF_DEEPSEEK_V4_FLASH_MODEL_ID
136+
119137
/**
120138
* Suffix for the base3 desktop roots. The single-loop agent is a different
121139
* agent with a different cost profile, so it gets its own root ids: spend and
@@ -569,6 +587,9 @@ export const FREEBUFF_ROOT_AGENT_IDS = [
569587
// The Desktop auto-run decider. Spawns nothing, but the hierarchy gate reads
570588
// this list for the ROOT itself, and a decision has no parent run to hang off.
571589
FREEBUFF_DESKTOP_AUTORUN_AGENT_ID,
590+
// The workspace profiler. Spawns nothing, and like the decider it runs
591+
// beside a turn with no parent run to hang off.
592+
FREEBUFF_WORKSPACE_PROFILER_AGENT_ID,
572593
] as const
573594
const FREEBUFF_ROOT_AGENT_ID_SET: ReadonlySet<string> = new Set(
574595
FREEBUFF_ROOT_AGENT_IDS,
@@ -759,8 +780,13 @@ export function isFreebuffHelperOnSessionModel(
759780
const { publisherId, agentId } = parseAgentId(fullAgentId)
760781
if (!agentId || (publisherId && publisherId !== 'codebuff')) return false
761782
return (
762-
FREEBUFF_LUNA_HELPER_AGENT_IDS.has(agentId) &&
763-
model === FREEBUFF_GPT_6_LUNA_MODEL_ID
783+
(FREEBUFF_LUNA_HELPER_AGENT_IDS.has(agentId) &&
784+
model === FREEBUFF_GPT_6_LUNA_MODEL_ID) ||
785+
// The workspace profiler is a root, but it is the same kind of traffic:
786+
// background work beside a session on whatever model that session runs,
787+
// pinned to its own cheap model.
788+
(agentId === FREEBUFF_WORKSPACE_PROFILER_AGENT_ID &&
789+
model === FREEBUFF_WORKSPACE_PROFILER_MODEL_ID)
764790
)
765791
}
766792

@@ -1004,6 +1030,12 @@ export const FREE_MODE_AGENT_MODELS: Record<string, Set<string>> = {
10041030
// the tab's own model, which is the one that tab's session was admitted with.
10051031
// Pinning it to a single model instead would 403 every tab on any other one.
10061032
[FREEBUFF_DESKTOP_AUTORUN_AGENT_ID]: FREEBUFF_DESKTOP_MODELS,
1033+
// The workspace profiler runs one model on every surface. Its requests must
1034+
// also look like the profiler (the completions route's helper check), so the
1035+
// id is not a free route onto DeepSeek V4 Flash for any other prompt.
1036+
[FREEBUFF_WORKSPACE_PROFILER_AGENT_ID]: new Set([
1037+
FREEBUFF_WORKSPACE_PROFILER_MODEL_ID,
1038+
]),
10071039
// A Projects thread's subagent runs its root's model, so it allows the
10081040
// root's set. This entry only lets it bill as the session it belongs to;
10091041
// who may run it at all is the completions route's Projects check.
@@ -1172,6 +1204,8 @@ export const FREEBUFF_ROOT_SYSTEM_PROMPT_OPENINGS = [
11721204
// ("you never edit files or run commands"). Position 0 is the worst place to
11731205
// say the wrong thing about who is reading.
11741206
'You are Buffy, the auto-run agent behind Freebuff Desktop.',
1207+
// common/src/workspace-profile/agent.ts — the workspace profiler.
1208+
'You are Buffy, the workspace profiler behind Freebuff.',
11751209
// LEGACY — base2's opening before 92371caa8 (2026-07-07), for installs that
11761210
// have not updated since.
11771211
'You are Buffy, a strategic assistant that orchestrates complex coding tasks through specialized sub-agents.',
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
import { describe, expect, test } from 'bun:test'
2+
3+
import {
4+
WORKSPACE_STATS_MAX_LANGUAGES,
5+
buildStoredWorkspaceProfile,
6+
computeWorkspaceStats,
7+
countLines,
8+
isTestPath,
9+
languageForPath,
10+
shouldCountLines,
11+
workspaceStatsSchema,
12+
} from '../stats'
13+
14+
describe('countLines', () => {
15+
test('counts a final line with or without a newline', () => {
16+
expect(countLines('')).toBe(0)
17+
expect(countLines('a')).toBe(1)
18+
expect(countLines('a\n')).toBe(1)
19+
expect(countLines('a\nb')).toBe(2)
20+
expect(countLines('a\nb\n')).toBe(2)
21+
expect(countLines('\n\n')).toBe(2)
22+
expect(countLines('a\r\nb\r\n')).toBe(2)
23+
})
24+
})
25+
26+
describe('languageForPath', () => {
27+
test('maps extensions and well-known file names', () => {
28+
expect(languageForPath('src/app.tsx')?.name).toBe('TypeScript')
29+
expect(languageForPath('C:\\proj\\main.PY')?.name).toBe('Python')
30+
expect(languageForPath('Dockerfile')?.name).toBe('Dockerfile')
31+
expect(languageForPath('deploy/Dockerfile.prod')?.name).toBe('Dockerfile')
32+
expect(languageForPath('Gemfile')?.name).toBe('Ruby')
33+
expect(languageForPath('README.md')).toEqual({
34+
name: 'Markdown',
35+
kind: 'prose',
36+
})
37+
expect(languageForPath('styles.css')?.kind).toBe('markup')
38+
expect(languageForPath('package.json')?.kind).toBe('data')
39+
})
40+
41+
test('skips lockfiles, minified bundles, maps and unknown types', () => {
42+
expect(languageForPath('package-lock.json')).toBeNull()
43+
expect(languageForPath('sub/bun.lock')).toBeNull()
44+
expect(languageForPath('Cargo.lock')).toBeNull()
45+
expect(languageForPath('dist/app.min.js')).toBeNull()
46+
expect(languageForPath('dist/app.js.map')).toBeNull()
47+
expect(languageForPath('logo.png')).toBeNull()
48+
expect(languageForPath('.gitignore')).toBeNull()
49+
expect(languageForPath('LICENSE')).toBeNull()
50+
expect(shouldCountLines('logo.png')).toBe(false)
51+
expect(shouldCountLines('index.ts')).toBe(true)
52+
})
53+
})
54+
55+
describe('isTestPath', () => {
56+
test('recognizes common test layouts', () => {
57+
expect(isTestPath('src/__tests__/a.ts')).toBe(true)
58+
expect(isTestPath('src/a.test.ts')).toBe(true)
59+
expect(isTestPath('src/a.spec.tsx')).toBe(true)
60+
expect(isTestPath('tests/test_api.py')).toBe(true)
61+
expect(isTestPath('pkg/handler_test.go')).toBe(true)
62+
expect(isTestPath('e2e\\login.ts')).toBe(true)
63+
expect(isTestPath('src/contest.ts')).toBe(false)
64+
expect(isTestPath('src/latest/index.ts')).toBe(false)
65+
})
66+
})
67+
68+
describe('computeWorkspaceStats', () => {
69+
test('totals files, test files, code lines and the language mix', () => {
70+
const stats = computeWorkspaceStats({
71+
fileCountIsLowerBound: false,
72+
files: [
73+
{ path: 'src/index.ts', lines: 100 },
74+
{ path: 'src/index.test.ts', lines: 40 },
75+
{ path: 'src/view.tsx', lines: 60 },
76+
{ path: 'scripts/build.py', lines: 30 },
77+
{ path: 'README.md', lines: 500 },
78+
{ path: 'package.json', lines: 20 },
79+
{ path: 'bun.lock', lines: 9000 },
80+
{ path: 'logo.png' },
81+
],
82+
})
83+
expect(workspaceStatsSchema.parse(stats)).toEqual(stats)
84+
expect(stats).toEqual({
85+
fileCount: 8,
86+
fileCountIsLowerBound: false,
87+
testFileCount: 1,
88+
codeLineCount: 230,
89+
lineCountIsLowerBound: false,
90+
languages: [
91+
{ name: 'Markdown', kind: 'prose', files: 1, lines: 500 },
92+
{ name: 'TypeScript', kind: 'code', files: 3, lines: 200 },
93+
{ name: 'Python', kind: 'code', files: 1, lines: 30 },
94+
{ name: 'JSON', kind: 'data', files: 1, lines: 20 },
95+
],
96+
})
97+
})
98+
99+
test('an unread file of a counted language makes lines a floor', () => {
100+
const stats = computeWorkspaceStats({
101+
fileCountIsLowerBound: false,
102+
files: [{ path: 'a.ts', lines: 10 }, { path: 'b.ts' }],
103+
})
104+
expect(stats.lineCountIsLowerBound).toBe(true)
105+
expect(stats.languages).toEqual([
106+
{ name: 'TypeScript', kind: 'code', files: 2, lines: 10 },
107+
])
108+
})
109+
110+
test('a capped listing makes both counts floors', () => {
111+
const stats = computeWorkspaceStats({
112+
fileCountIsLowerBound: true,
113+
files: [{ path: 'a.ts', lines: 10 }],
114+
})
115+
expect(stats.fileCountIsLowerBound).toBe(true)
116+
expect(stats.lineCountIsLowerBound).toBe(true)
117+
})
118+
119+
test('keeps the top languages by lines, then files', () => {
120+
const extensions = [
121+
'.ts',
122+
'.py',
123+
'.go',
124+
'.rs',
125+
'.java',
126+
'.kt',
127+
'.swift',
128+
'.c',
129+
'.cpp',
130+
'.cs',
131+
'.php',
132+
'.dart',
133+
'.lua',
134+
'.r',
135+
'.ex',
136+
'.hs',
137+
'.ml',
138+
'.zig',
139+
'.nim',
140+
'.sol',
141+
'.pl',
142+
'.sh',
143+
'.sql',
144+
'.vue',
145+
'.svelte',
146+
]
147+
const stats = computeWorkspaceStats({
148+
fileCountIsLowerBound: false,
149+
files: extensions.map((ext, i) => ({ path: `f${ext}`, lines: i + 1 })),
150+
})
151+
expect(stats.languages).toHaveLength(WORKSPACE_STATS_MAX_LANGUAGES)
152+
expect(stats.languages[0]).toEqual({
153+
name: 'Svelte',
154+
kind: 'code',
155+
files: 1,
156+
lines: extensions.length,
157+
})
158+
expect(workspaceStatsSchema.safeParse(stats).success).toBe(true)
159+
})
160+
})
161+
162+
describe('buildStoredWorkspaceProfile', () => {
163+
const stats = computeWorkspaceStats({
164+
files: [
165+
{ path: 'src/a.ts', lines: 10 },
166+
{ path: 'src/a.test.ts', lines: 4 },
167+
],
168+
fileCountIsLowerBound: false,
169+
})
170+
171+
test('adds the counts as the size section beside the answers', () => {
172+
expect(
173+
buildStoredWorkspaceProfile(
174+
{ languages: { languages: ['TypeScript'] } },
175+
stats,
176+
),
177+
).toEqual({
178+
languages: { languages: ['TypeScript'] },
179+
size: {
180+
file_count: 2,
181+
file_count_is_lower_bound: false,
182+
test_file_count: 1,
183+
code_line_count: 14,
184+
line_count_is_lower_bound: false,
185+
languages: [{ name: 'TypeScript', kind: 'code', files: 2, lines: 14 }],
186+
},
187+
})
188+
})
189+
190+
test('size is always the counts, whatever the answers carried', () => {
191+
// A model can put anything in its answer; the stored size never comes from it.
192+
const answers = { size: { file_count: '999999' } }
193+
const stored = buildStoredWorkspaceProfile(answers, stats)
194+
expect(stored.size).toMatchObject({ file_count: 2 })
195+
})
196+
})

0 commit comments

Comments
 (0)