Skip to content

Commit bd2b44b

Browse files
authored
Merge pull request #208 from skulidropek/issue-207
Respect agent-specific project rules in cloned repos
2 parents 546c727 + 328b9a7 commit bd2b44b

File tree

7 files changed

+147
-0
lines changed

7 files changed

+147
-0
lines changed

packages/app/tests/docker-git/entrypoint-auth.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,24 @@ describe("renderEntrypoint auth bridge", () => {
7979
expect(entrypoint).toContain("CLAUDE_GLOBAL_PROMPT_FILE=\"/home/dev/.claude/CLAUDE.md\"")
8080
expect(entrypoint).toContain("CLAUDE_AUTO_SYSTEM_PROMPT=\"${CLAUDE_AUTO_SYSTEM_PROMPT:-1}\"")
8181
expect(entrypoint).toContain("docker-git-managed:claude-md")
82+
expect(entrypoint).toContain("docker_git_sync_project_codex_skills()")
83+
expect(entrypoint).toContain('project_skills_root="$codex_home/skills/.docker-git-project"')
84+
expect(entrypoint).toContain("docker_git_prepare_active_agent_project_rules()")
85+
expect(entrypoint).toContain('docker_git_detect_claude_project_rules()')
86+
expect(entrypoint).toContain('docker_git_detect_gemini_project_rules()')
87+
expect(entrypoint).toContain('"codex")')
88+
expect(entrypoint).toContain('"claude")')
89+
expect(entrypoint).toContain('"gemini")')
90+
expect(entrypoint).toContain('"20-agents-skills::.agents/skills"')
91+
expect(entrypoint).toContain('"30-agents-dot-skills::.agents/.skills"')
92+
expect(entrypoint).toContain('"80-codex-skills::.codex/skills"')
93+
expect(entrypoint).toContain('"90-codex-dot-skills::.codex/.skills"')
94+
expect(entrypoint).not.toContain('"40-claude-skills::.claude/skills"')
95+
expect(entrypoint).toContain('$project_dir/.claude/settings.json')
96+
expect(entrypoint).toContain('$project_dir/.claude/agents')
97+
expect(entrypoint).toContain('$project_dir/.gemini/settings.json')
98+
expect(entrypoint).toContain('$project_dir/.gemini/commands')
99+
expect(entrypoint).toContain('$project_dir/.gemini/skills')
82100
expect(entrypoint).toContain(
83101
"SUBAGENTS_LINE=\"Для решения задач обязательно используй subagents. Сам агент обязан выполнять финальную проверку, интеграцию и валидацию результата перед ответом пользователю.\""
84102
)

packages/lib/src/core/templates-entrypoint.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { renderEntrypointClaudeConfig } from "./templates-entrypoint/claude.js"
1515
import {
1616
renderEntrypointAgentsNotice,
1717
renderEntrypointCodexHome,
18+
renderEntrypointProjectCodexSkillsSync,
1819
renderEntrypointCodexResumeHint,
1920
renderEntrypointCodexSharedAuth,
2021
renderEntrypointMcpPlaywright
@@ -24,6 +25,7 @@ import { renderEntrypointGeminiConfig } from "./templates-entrypoint/gemini.js"
2425
import { renderEntrypointGitConfig, renderEntrypointGitHooks } from "./templates-entrypoint/git.js"
2526
import { renderEntrypointDockerGitBootstrap } from "./templates-entrypoint/nested-docker-git.js"
2627
import { renderEntrypointOpenCodeConfig } from "./templates-entrypoint/opencode.js"
28+
import { renderEntrypointProjectAgentRules } from "./templates-entrypoint/project-rules.js"
2729
import { renderEntrypointBackgroundTasks } from "./templates-entrypoint/tasks.js"
2830
import {
2931
renderEntrypointBashCompletion,
@@ -51,6 +53,8 @@ export const renderEntrypoint = (config: TemplateConfig): string =>
5153
renderEntrypointInputRc(config),
5254
renderEntrypointZshConfig(),
5355
renderEntrypointCodexResumeHint(config),
56+
renderEntrypointProjectCodexSkillsSync(config),
57+
renderEntrypointProjectAgentRules(),
5458
renderEntrypointAgentsNotice(config),
5559
renderEntrypointDockerSocket(config),
5660
renderEntrypointGitConfig(config),

packages/lib/src/core/templates-entrypoint/codex.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,51 @@ export const renderEntrypointMcpPlaywright = (config: TemplateConfig): string =>
121121
.replaceAll("__CODEX_HOME__", config.codexHome)
122122
.replaceAll("__SERVICE_NAME__", config.serviceName)
123123

124+
const entrypointProjectCodexSkillsSyncTemplate = String.raw`# Mirror project-owned Codex skill trees into CODEX_HOME without overwriting global skills.
125+
docker_git_sync_project_codex_skills() {
126+
local codex_home="${"$"}{CODEX_HOME:-__CODEX_HOME__}"
127+
local project_dir="${"$"}{TARGET_DIR:-}"
128+
local project_skills_root="$codex_home/skills/.docker-git-project"
129+
local linked=0
130+
local spec=""
131+
local mount_name=""
132+
local relative_path=""
133+
134+
if [[ -z "$project_dir" || ! -d "$project_dir" ]]; then
135+
return 0
136+
fi
137+
138+
mkdir -p "$codex_home/skills"
139+
rm -rf "$project_skills_root"
140+
mkdir -p "$project_skills_root"
141+
142+
# Priority goes from generic/shared skill trees -> Codex-specific trees.
143+
for spec in \
144+
"10-root-skills::.skills" \
145+
"20-agents-skills::.agents/skills" \
146+
"30-agents-dot-skills::.agents/.skills" \
147+
"80-codex-skills::.codex/skills" \
148+
"90-codex-dot-skills::.codex/.skills"; do
149+
mount_name="${"$"}{spec%%::*}"
150+
relative_path="${"$"}{spec#*::}"
151+
152+
if [[ -d "$project_dir/$relative_path" ]]; then
153+
ln -sfn "$project_dir/$relative_path" "$project_skills_root/$mount_name"
154+
chown -h 1000:1000 "$project_skills_root/$mount_name" 2>/dev/null || true
155+
linked=1
156+
fi
157+
done
158+
159+
chown 1000:1000 "$codex_home/skills" "$project_skills_root" 2>/dev/null || true
160+
161+
if [[ "$linked" -eq 1 ]]; then
162+
echo "[codex-skills] linked project skill trees into $project_skills_root"
163+
fi
164+
}`
165+
166+
export const renderEntrypointProjectCodexSkillsSync = (config: TemplateConfig): string =>
167+
entrypointProjectCodexSkillsSyncTemplate.replaceAll("__CODEX_HOME__", config.codexHome)
168+
124169
const entrypointAgentsNoticeTemplate = String.raw`# Ensure global AGENTS.md exists for container context
125170
AGENTS_PATH="__CODEX_HOME__/AGENTS.md"
126171
LEGACY_AGENTS_PATH="/home/__SSH_USER__/AGENTS.md"
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// CHANGE: separate project rule preparation by active agent mode
2+
// WHY: Codex, Claude Code, and Gemini CLI each have different native project-level config models
3+
// REF: issue-207
4+
// PURITY: CORE
5+
// INVARIANT: Codex gets a bridge for skills that live outside CODEX_HOME; Claude/Gemini stay on native project-local discovery
6+
// COMPLEXITY: O(1)
7+
const entrypointProjectAgentRulesTemplate = String.raw`# Prepare project-local rules using each agent's native conventions.
8+
docker_git_detect_claude_project_rules() {
9+
local project_dir="${"$"}{TARGET_DIR:-}"
10+
11+
if [[ -z "$project_dir" || ! -d "$project_dir" ]]; then
12+
return 0
13+
fi
14+
15+
if [[ -f "$project_dir/CLAUDE.md" \
16+
|| -f "$project_dir/.claude/CLAUDE.md" \
17+
|| -f "$project_dir/.claude/settings.json" \
18+
|| -d "$project_dir/.claude/agents" \
19+
|| -f "$project_dir/.mcp.json" ]]; then
20+
echo "[claude] project-local Claude rules available in $project_dir"
21+
fi
22+
}
23+
24+
docker_git_detect_gemini_project_rules() {
25+
local project_dir="${"$"}{TARGET_DIR:-}"
26+
27+
if [[ -z "$project_dir" || ! -d "$project_dir" ]]; then
28+
return 0
29+
fi
30+
31+
if [[ -f "$project_dir/GEMINI.md" \
32+
|| -f "$project_dir/.gemini/settings.json" \
33+
|| -d "$project_dir/.gemini/commands" \
34+
|| -d "$project_dir/.gemini/skills" \
35+
|| -d "$project_dir/.agents/skills" ]]; then
36+
echo "[gemini] project-local Gemini rules available in $project_dir"
37+
fi
38+
}
39+
40+
docker_git_prepare_active_agent_project_rules() {
41+
case "$AGENT_MODE" in
42+
"codex")
43+
docker_git_sync_project_codex_skills
44+
;;
45+
"claude")
46+
docker_git_detect_claude_project_rules
47+
;;
48+
"gemini")
49+
docker_git_detect_gemini_project_rules
50+
;;
51+
*)
52+
docker_git_sync_project_codex_skills
53+
docker_git_detect_claude_project_rules
54+
docker_git_detect_gemini_project_rules
55+
;;
56+
esac
57+
}`
58+
59+
export const renderEntrypointProjectAgentRules = (): string => entrypointProjectAgentRulesTemplate

packages/lib/src/core/templates-entrypoint/tasks.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,5 +221,9 @@ ${renderEntrypointAutoUpdate()}
221221
222222
${renderEntrypointClone(config)}
223223
224+
if [[ "$CLONE_OK" -eq 1 ]]; then
225+
docker_git_prepare_active_agent_project_rules
226+
fi
227+
224228
${renderAgentLaunch(config)}
225229
) &`

packages/lib/tests/usecases/apply.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,11 @@ describe("applyProjectFiles", () => {
153153
expect(composeAfter).toContain("cpus:")
154154
expect(composeAfter).toContain('mem_limit: "')
155155

156+
const entrypointAfter = yield* _(fs.readFileString(path.join(outDir, "entrypoint.sh")))
157+
expect(entrypointAfter).toContain("docker_git_prepare_active_agent_project_rules()")
158+
expect(entrypointAfter).toContain('"20-agents-skills::.agents/skills"')
159+
expect(entrypointAfter).toContain('$project_dir/.claude/settings.json')
160+
156161
const configAfter = yield* _(fs.readFileString(configPath))
157162
expect(configAfter).toContain('"cpuLimit": "30%"')
158163
expect(configAfter).toContain('"ramLimit": "30%"')

packages/lib/tests/usecases/prepare-files.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,15 @@ describe("prepareProjectFiles", () => {
175175
expect(entrypoint).toContain('. /etc/profile 2>/dev/null || true;')
176176
expect(entrypoint).toContain("codex exec")
177177
expect(entrypoint).not.toContain("codex --approval-mode full-auto")
178+
expect(entrypoint).toContain("docker_git_sync_project_codex_skills()")
179+
expect(entrypoint).toContain('project_skills_root="$codex_home/skills/.docker-git-project"')
180+
expect(entrypoint).toContain("docker_git_prepare_active_agent_project_rules()")
181+
expect(entrypoint).toContain('"10-root-skills::.skills"')
182+
expect(entrypoint).toContain('"20-agents-skills::.agents/skills"')
183+
expect(entrypoint).toContain('"90-codex-dot-skills::.codex/.skills"')
184+
expect(entrypoint).not.toContain('"40-claude-skills::.claude/skills"')
185+
expect(entrypoint).toContain('$project_dir/.claude/settings.json')
186+
expect(entrypoint).toContain('$project_dir/.gemini/settings.json')
178187
expect(entrypoint).toContain("docker_git_repair_dns() {")
179188
expect(entrypoint).toContain('local test_domain="github.com"')
180189
expect(entrypoint).toContain('local fallback_dns="8.8.8.8 8.8.4.4 1.1.1.1"')
@@ -186,6 +195,9 @@ describe("prepareProjectFiles", () => {
186195
expect(entrypoint).toContain("cat > \"$MOVE_SCRIPT\" << 'EOFMOVE'")
187196
expect(entrypoint).toMatch(/\nEOFMOVE\n\s*chmod \+x "\$MOVE_SCRIPT"/)
188197
expect(entrypoint).not.toContain("\n EOFMOVE\n")
198+
expect(entrypoint).toContain(
199+
"if [[ \"$CLONE_OK\" -eq 1 ]]; then\n docker_git_prepare_active_agent_project_rules\nfi"
200+
)
189201
expect(composeBefore).toContain("container_name: dg-test")
190202
expect(composeBefore).toContain("restart: unless-stopped")
191203
expect(composeBefore).toContain(":/home/dev/.docker-git")

0 commit comments

Comments
 (0)