Skip to content

Commit a0accfb

Browse files
committed
feat: align AGENTS.md loading across provider protocols (#225)
* feat: align AGENTS.md across provider protocols * feat: show AGENTS.md instructions in dashboard * fix: preserve AGENTS.md turn snapshots
1 parent f58a458 commit a0accfb

105 files changed

Lines changed: 2068 additions & 855 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

desktop/src/main/tests/workspaceSetup.test.ts

Lines changed: 48 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -281,21 +281,14 @@ describe('shouldRouteWorkspaceThroughSetupBeforeAppServerStart', () => {
281281
})
282282

283283
describe('workspace setup bootstrap import detection', () => {
284-
it('detects nearest AGENTS.md and CLAUDE.md sources in AGENTS.md-first order', () => {
284+
it('detects the nearest CLAUDE.md source when the workspace has no root AGENTS.md', () => {
285285
const workspace = createTempWorkspace()
286286
const child = join(workspace, 'packages', 'app')
287287
mkdirSync(child, { recursive: true })
288-
writeFileSync(join(workspace, 'AGENTS.md'), 'root agents', 'utf8')
289-
writeFileSync(join(child, 'AGENTS.md'), 'child agents', 'utf8')
288+
mkdirSync(join(workspace, '.git'))
290289
writeFileSync(join(workspace, 'CLAUDE.md'), 'claude', 'utf8')
291290

292291
expect(detectWorkspaceSetupBootstrapImportSources(child)).toEqual([
293-
{
294-
id: 'codex',
295-
fileName: 'AGENTS.md',
296-
path: join(child, 'AGENTS.md'),
297-
relativePath: 'AGENTS.md'
298-
},
299292
{
300293
id: 'claude',
301294
fileName: 'CLAUDE.md',
@@ -309,7 +302,7 @@ describe('workspace setup bootstrap import detection', () => {
309302
const workspace = createTempWorkspace()
310303
const userHome = createTempWorkspace()
311304
const userConfigPath = join(userHome, '.craft', 'config.json')
312-
writeFileSync(join(workspace, 'AGENTS.md'), 'agents', 'utf8')
305+
writeFileSync(join(workspace, 'CLAUDE.md'), 'claude', 'utf8')
313306
writeJson(userConfigPath, {
314307
Providers: {
315308
openai: {
@@ -325,8 +318,8 @@ describe('workspace setup bootstrap import detection', () => {
325318
status: 'needs-setup',
326319
bootstrapImportSources: [
327320
expect.objectContaining({
328-
id: 'codex',
329-
fileName: 'AGENTS.md'
321+
id: 'claude',
322+
fileName: 'CLAUDE.md'
330323
})
331324
]
332325
})
@@ -341,19 +334,58 @@ describe('workspace setup bootstrap import detection', () => {
341334
.not.toHaveProperty('bootstrapImportSources')
342335
})
343336

344-
it('copies the selected source over .craft/AGENTS.md and records metadata', () => {
337+
it('copies the selected source to root AGENTS.md and records metadata', () => {
345338
const workspace = createTempWorkspace()
346339
writeFileSync(join(workspace, 'CLAUDE.md'), '# Claude rules\n', 'utf8')
347-
mkdirSync(join(workspace, '.craft'), { recursive: true })
348-
writeFileSync(join(workspace, '.craft', 'AGENTS.md'), '# DotCraft template\n', 'utf8')
349340

350341
expect(applyWorkspaceSetupBootstrapImport(workspace, 'claude')).toEqual({
351342
sourceId: 'claude',
352343
status: 'success'
353344
})
354-
expect(readFileSync(join(workspace, '.craft', 'AGENTS.md'), 'utf8')).toBe('# Claude rules\n')
345+
expect(readFileSync(join(workspace, 'AGENTS.md'), 'utf8')).toBe('# Claude rules\n')
355346
expect(existsSync(join(workspace, '.craft', 'imports', 'bootstrap-import.json'))).toBe(true)
356347
})
348+
349+
it('does not search above the nearest repository root or above an unversioned workspace', () => {
350+
const parent = createTempWorkspace()
351+
writeFileSync(join(parent, 'CLAUDE.md'), 'outside', 'utf8')
352+
const repository = join(parent, 'repository')
353+
const child = join(repository, 'packages', 'app')
354+
mkdirSync(join(repository, '.git'), { recursive: true })
355+
mkdirSync(child, { recursive: true })
356+
357+
expect(detectWorkspaceSetupBootstrapImportSources(child)).toEqual([])
358+
359+
const worktree = join(parent, 'worktree')
360+
const worktreeChild = join(worktree, 'packages', 'app')
361+
mkdirSync(worktreeChild, { recursive: true })
362+
writeFileSync(join(worktree, '.git'), 'gitdir: ../repository/.git/worktrees/example', 'utf8')
363+
expect(detectWorkspaceSetupBootstrapImportSources(worktreeChild)).toEqual([])
364+
365+
const unversioned = join(parent, 'unversioned')
366+
mkdirSync(unversioned)
367+
expect(detectWorkspaceSetupBootstrapImportSources(unversioned)).toEqual([])
368+
})
369+
370+
it('does not offer or overwrite a root AGENTS.md', () => {
371+
const workspace = createTempWorkspace()
372+
writeFileSync(join(workspace, 'CLAUDE.md'), '# Claude rules\n', 'utf8')
373+
writeFileSync(join(workspace, 'AGENTS.md'), '# Existing rules\n', 'utf8')
374+
375+
expect(detectWorkspaceSetupBootstrapImportSources(workspace)).toEqual([])
376+
expect(applyWorkspaceSetupBootstrapImport(workspace, 'claude')).toMatchObject({ status: 'failed' })
377+
expect(readFileSync(join(workspace, 'AGENTS.md'), 'utf8')).toBe('# Existing rules\n')
378+
})
379+
380+
it('does not offer or copy beside a root AGENTS.override.md', () => {
381+
const workspace = createTempWorkspace()
382+
writeFileSync(join(workspace, 'CLAUDE.md'), '# Claude rules\n', 'utf8')
383+
writeFileSync(join(workspace, 'AGENTS.override.md'), '# Override rules\n', 'utf8')
384+
385+
expect(detectWorkspaceSetupBootstrapImportSources(workspace)).toEqual([])
386+
expect(applyWorkspaceSetupBootstrapImport(workspace, 'claude')).toMatchObject({ status: 'failed' })
387+
expect(existsSync(join(workspace, 'AGENTS.md'))).toBe(false)
388+
})
357389
})
358390

359391
describe('createUniqueSetupProviderId', () => {

desktop/src/main/workspaceSetup.ts

Lines changed: 30 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { execFile, spawn } from 'child_process'
2-
import { copyFileSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'fs'
2+
import { constants, copyFileSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'fs'
33
import { homedir } from 'os'
44
import { dirname, join, relative, resolve } from 'path'
55
import { resolveBinaryLocation } from './AppServerManager'
@@ -417,23 +417,32 @@ function isRegularFile(path: string): boolean {
417417
}
418418
}
419419

420-
function findNearestSetupImportFile(
421-
workspacePath: string,
422-
fileName: 'AGENTS.md' | 'CLAUDE.md'
423-
): string | null {
424-
let current = resolve(workspacePath)
420+
function findNearestSetupImportFile(workspacePath: string, fileName: 'CLAUDE.md'): string | null {
421+
const workspaceRoot = resolve(workspacePath)
422+
let repositoryRoot: string | null = null
423+
let current = workspaceRoot
424+
425+
while (true) {
426+
if (existsSync(join(current, '.git'))) {
427+
repositoryRoot = current
428+
break
429+
}
430+
const parent = dirname(current)
431+
if (parent === current) break
432+
current = parent
433+
}
434+
435+
current = workspaceRoot
436+
const searchRoot = repositoryRoot ?? workspaceRoot
425437

426438
while (true) {
427439
const candidate = join(current, fileName)
428440
if (isRegularFile(candidate)) {
429441
return candidate
430442
}
431443

432-
const parent = dirname(current)
433-
if (parent === current) {
434-
return null
435-
}
436-
current = parent
444+
if (current === searchRoot) return null
445+
current = dirname(current)
437446
}
438447
}
439448

@@ -448,19 +457,11 @@ export function detectWorkspaceSetupBootstrapImportSources(
448457
const trimmed = workspacePath.trim()
449458
if (!trimmed) return []
450459

451-
const agentsPath = findNearestSetupImportFile(trimmed, 'AGENTS.md')
460+
if (existsSync(join(trimmed, 'AGENTS.md')) || existsSync(join(trimmed, 'AGENTS.override.md'))) return []
461+
452462
const claudePath = findNearestSetupImportFile(trimmed, 'CLAUDE.md')
453463
const sources: WorkspaceSetupBootstrapImportSource[] = []
454464

455-
if (agentsPath) {
456-
sources.push({
457-
id: 'codex',
458-
fileName: 'AGENTS.md',
459-
path: agentsPath,
460-
relativePath: sourceRelativePath(trimmed, agentsPath)
461-
})
462-
}
463-
464465
if (claudePath) {
465466
sources.push({
466467
id: 'claude',
@@ -588,11 +589,14 @@ export function applyWorkspaceSetupBootstrapImport(
588589
}
589590

590591
const craftPath = join(workspacePath, '.craft')
591-
const destinationPath = join(craftPath, 'AGENTS.md')
592+
const destinationPath = join(workspacePath, 'AGENTS.md')
593+
const overridePath = join(workspacePath, 'AGENTS.override.md')
592594

593595
try {
594-
mkdirSync(craftPath, { recursive: true })
595-
copyFileSync(source.path, destinationPath)
596+
if (existsSync(overridePath)) {
597+
throw new Error('Workspace root instructions already exist.')
598+
}
599+
copyFileSync(source.path, destinationPath, constants.COPYFILE_EXCL)
596600
} catch (error) {
597601
return {
598602
sourceId,
@@ -614,7 +618,7 @@ export function applyWorkspaceSetupBootstrapImport(
614618
path: source.path,
615619
relativePath: source.relativePath
616620
},
617-
destination: '.craft/AGENTS.md',
621+
destination: 'AGENTS.md',
618622
status: 'success'
619623
}, null, 2)}\n`,
620624
'utf8'
@@ -639,11 +643,7 @@ export function runWorkspaceSetup(
639643
settings: AppSettings
640644
): Promise<WorkspaceSetupResult> {
641645
const binaryPath = resolveDesktopBinary(settings)
642-
const args = [
643-
'setup',
644-
'--profile',
645-
request.profile
646-
]
646+
const args = ['setup']
647647

648648
appendProviderArgs(args, request)
649649
if (request.setAsUserDefault) {

desktop/src/renderer/assets/profile-logos/dotcraft-developer.svg

Lines changed: 0 additions & 72 deletions
This file was deleted.

desktop/src/renderer/assets/profile-logos/dotcraft-personal-assistant.svg

Lines changed: 0 additions & 70 deletions
This file was deleted.

0 commit comments

Comments
 (0)