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
18 changes: 18 additions & 0 deletions packages/app/tests/docker-git/entrypoint-auth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,24 @@ describe("renderEntrypoint auth bridge", () => {
expect(entrypoint).toContain("CLAUDE_GLOBAL_PROMPT_FILE=\"/home/dev/.claude/CLAUDE.md\"")
expect(entrypoint).toContain("CLAUDE_AUTO_SYSTEM_PROMPT=\"${CLAUDE_AUTO_SYSTEM_PROMPT:-1}\"")
expect(entrypoint).toContain("docker-git-managed:claude-md")
expect(entrypoint).toContain("docker_git_sync_project_codex_skills()")
expect(entrypoint).toContain('project_skills_root="$codex_home/skills/.docker-git-project"')
expect(entrypoint).toContain("docker_git_prepare_active_agent_project_rules()")
expect(entrypoint).toContain('docker_git_detect_claude_project_rules()')
expect(entrypoint).toContain('docker_git_detect_gemini_project_rules()')
expect(entrypoint).toContain('"codex")')
expect(entrypoint).toContain('"claude")')
expect(entrypoint).toContain('"gemini")')
expect(entrypoint).toContain('"20-agents-skills::.agents/skills"')
expect(entrypoint).toContain('"30-agents-dot-skills::.agents/.skills"')
expect(entrypoint).toContain('"80-codex-skills::.codex/skills"')
expect(entrypoint).toContain('"90-codex-dot-skills::.codex/.skills"')
expect(entrypoint).not.toContain('"40-claude-skills::.claude/skills"')
expect(entrypoint).toContain('$project_dir/.claude/settings.json')
expect(entrypoint).toContain('$project_dir/.claude/agents')
expect(entrypoint).toContain('$project_dir/.gemini/settings.json')
expect(entrypoint).toContain('$project_dir/.gemini/commands')
expect(entrypoint).toContain('$project_dir/.gemini/skills')
expect(entrypoint).toContain(
"SUBAGENTS_LINE=\"Для решения задач обязательно используй subagents. Сам агент обязан выполнять финальную проверку, интеграцию и валидацию результата перед ответом пользователю.\""
)
Expand Down
4 changes: 4 additions & 0 deletions packages/lib/src/core/templates-entrypoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { renderEntrypointClaudeConfig } from "./templates-entrypoint/claude.js"
import {
renderEntrypointAgentsNotice,
renderEntrypointCodexHome,
renderEntrypointProjectCodexSkillsSync,
renderEntrypointCodexResumeHint,
renderEntrypointCodexSharedAuth,
renderEntrypointMcpPlaywright
Expand All @@ -24,6 +25,7 @@ import { renderEntrypointGeminiConfig } from "./templates-entrypoint/gemini.js"
import { renderEntrypointGitConfig, renderEntrypointGitHooks } from "./templates-entrypoint/git.js"
import { renderEntrypointDockerGitBootstrap } from "./templates-entrypoint/nested-docker-git.js"
import { renderEntrypointOpenCodeConfig } from "./templates-entrypoint/opencode.js"
import { renderEntrypointProjectAgentRules } from "./templates-entrypoint/project-rules.js"
import { renderEntrypointBackgroundTasks } from "./templates-entrypoint/tasks.js"
import {
renderEntrypointBashCompletion,
Expand Down Expand Up @@ -51,6 +53,8 @@ export const renderEntrypoint = (config: TemplateConfig): string =>
renderEntrypointInputRc(config),
renderEntrypointZshConfig(),
renderEntrypointCodexResumeHint(config),
renderEntrypointProjectCodexSkillsSync(config),
renderEntrypointProjectAgentRules(),
renderEntrypointAgentsNotice(config),
renderEntrypointDockerSocket(config),
renderEntrypointGitConfig(config),
Expand Down
45 changes: 45 additions & 0 deletions packages/lib/src/core/templates-entrypoint/codex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,51 @@ export const renderEntrypointMcpPlaywright = (config: TemplateConfig): string =>
.replaceAll("__CODEX_HOME__", config.codexHome)
.replaceAll("__SERVICE_NAME__", config.serviceName)

const entrypointProjectCodexSkillsSyncTemplate = String.raw`# Mirror project-owned Codex skill trees into CODEX_HOME without overwriting global skills.
docker_git_sync_project_codex_skills() {
local codex_home="${"$"}{CODEX_HOME:-__CODEX_HOME__}"
local project_dir="${"$"}{TARGET_DIR:-}"
local project_skills_root="$codex_home/skills/.docker-git-project"
local linked=0
local spec=""
local mount_name=""
local relative_path=""

if [[ -z "$project_dir" || ! -d "$project_dir" ]]; then
return 0
fi

mkdir -p "$codex_home/skills"
rm -rf "$project_skills_root"
mkdir -p "$project_skills_root"

# Priority goes from generic/shared skill trees -> Codex-specific trees.
for spec in \
"10-root-skills::.skills" \
"20-agents-skills::.agents/skills" \
"30-agents-dot-skills::.agents/.skills" \
"80-codex-skills::.codex/skills" \
"90-codex-dot-skills::.codex/.skills"; do
mount_name="${"$"}{spec%%::*}"
relative_path="${"$"}{spec#*::}"

if [[ -d "$project_dir/$relative_path" ]]; then
ln -sfn "$project_dir/$relative_path" "$project_skills_root/$mount_name"
chown -h 1000:1000 "$project_skills_root/$mount_name" 2>/dev/null || true
linked=1
fi
done

chown 1000:1000 "$codex_home/skills" "$project_skills_root" 2>/dev/null || true

if [[ "$linked" -eq 1 ]]; then
echo "[codex-skills] linked project skill trees into $project_skills_root"
fi
}`

export const renderEntrypointProjectCodexSkillsSync = (config: TemplateConfig): string =>
entrypointProjectCodexSkillsSyncTemplate.replaceAll("__CODEX_HOME__", config.codexHome)

const entrypointAgentsNoticeTemplate = String.raw`# Ensure global AGENTS.md exists for container context
AGENTS_PATH="__CODEX_HOME__/AGENTS.md"
LEGACY_AGENTS_PATH="/home/__SSH_USER__/AGENTS.md"
Expand Down
59 changes: 59 additions & 0 deletions packages/lib/src/core/templates-entrypoint/project-rules.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// CHANGE: separate project rule preparation by active agent mode
// WHY: Codex, Claude Code, and Gemini CLI each have different native project-level config models
// REF: issue-207
// PURITY: CORE
// INVARIANT: Codex gets a bridge for skills that live outside CODEX_HOME; Claude/Gemini stay on native project-local discovery
// COMPLEXITY: O(1)
const entrypointProjectAgentRulesTemplate = String.raw`# Prepare project-local rules using each agent's native conventions.
docker_git_detect_claude_project_rules() {
local project_dir="${"$"}{TARGET_DIR:-}"

if [[ -z "$project_dir" || ! -d "$project_dir" ]]; then
return 0
fi

if [[ -f "$project_dir/CLAUDE.md" \
|| -f "$project_dir/.claude/CLAUDE.md" \
|| -f "$project_dir/.claude/settings.json" \
|| -d "$project_dir/.claude/agents" \
|| -f "$project_dir/.mcp.json" ]]; then
echo "[claude] project-local Claude rules available in $project_dir"
fi
}

docker_git_detect_gemini_project_rules() {
local project_dir="${"$"}{TARGET_DIR:-}"

if [[ -z "$project_dir" || ! -d "$project_dir" ]]; then
return 0
fi

if [[ -f "$project_dir/GEMINI.md" \
|| -f "$project_dir/.gemini/settings.json" \
|| -d "$project_dir/.gemini/commands" \
|| -d "$project_dir/.gemini/skills" \
|| -d "$project_dir/.agents/skills" ]]; then
echo "[gemini] project-local Gemini rules available in $project_dir"
fi
}

docker_git_prepare_active_agent_project_rules() {
case "$AGENT_MODE" in
"codex")
docker_git_sync_project_codex_skills
;;
"claude")
docker_git_detect_claude_project_rules
;;
"gemini")
docker_git_detect_gemini_project_rules
;;
*)
docker_git_sync_project_codex_skills
docker_git_detect_claude_project_rules
docker_git_detect_gemini_project_rules
;;
esac
}`

export const renderEntrypointProjectAgentRules = (): string => entrypointProjectAgentRulesTemplate
4 changes: 4 additions & 0 deletions packages/lib/src/core/templates-entrypoint/tasks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,5 +221,9 @@ ${renderEntrypointAutoUpdate()}

${renderEntrypointClone(config)}

if [[ "$CLONE_OK" -eq 1 ]]; then
docker_git_prepare_active_agent_project_rules
fi

${renderAgentLaunch(config)}
) &`
5 changes: 5 additions & 0 deletions packages/lib/tests/usecases/apply.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,11 @@ describe("applyProjectFiles", () => {
expect(composeAfter).toContain("cpus:")
expect(composeAfter).toContain('mem_limit: "')

const entrypointAfter = yield* _(fs.readFileString(path.join(outDir, "entrypoint.sh")))
expect(entrypointAfter).toContain("docker_git_prepare_active_agent_project_rules()")
expect(entrypointAfter).toContain('"20-agents-skills::.agents/skills"')
expect(entrypointAfter).toContain('$project_dir/.claude/settings.json')

const configAfter = yield* _(fs.readFileString(configPath))
expect(configAfter).toContain('"cpuLimit": "30%"')
expect(configAfter).toContain('"ramLimit": "30%"')
Expand Down
12 changes: 12 additions & 0 deletions packages/lib/tests/usecases/prepare-files.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,15 @@ describe("prepareProjectFiles", () => {
expect(entrypoint).toContain('. /etc/profile 2>/dev/null || true;')
expect(entrypoint).toContain("codex exec")
expect(entrypoint).not.toContain("codex --approval-mode full-auto")
expect(entrypoint).toContain("docker_git_sync_project_codex_skills()")
expect(entrypoint).toContain('project_skills_root="$codex_home/skills/.docker-git-project"')
expect(entrypoint).toContain("docker_git_prepare_active_agent_project_rules()")
expect(entrypoint).toContain('"10-root-skills::.skills"')
expect(entrypoint).toContain('"20-agents-skills::.agents/skills"')
expect(entrypoint).toContain('"90-codex-dot-skills::.codex/.skills"')
expect(entrypoint).not.toContain('"40-claude-skills::.claude/skills"')
expect(entrypoint).toContain('$project_dir/.claude/settings.json')
expect(entrypoint).toContain('$project_dir/.gemini/settings.json')
expect(entrypoint).toContain("docker_git_repair_dns() {")
expect(entrypoint).toContain('local test_domain="github.com"')
expect(entrypoint).toContain('local fallback_dns="8.8.8.8 8.8.4.4 1.1.1.1"')
Expand All @@ -186,6 +195,9 @@ describe("prepareProjectFiles", () => {
expect(entrypoint).toContain("cat > \"$MOVE_SCRIPT\" << 'EOFMOVE'")
expect(entrypoint).toMatch(/\nEOFMOVE\n\s*chmod \+x "\$MOVE_SCRIPT"/)
expect(entrypoint).not.toContain("\n EOFMOVE\n")
expect(entrypoint).toContain(
"if [[ \"$CLONE_OK\" -eq 1 ]]; then\n docker_git_prepare_active_agent_project_rules\nfi"
)
expect(composeBefore).toContain("container_name: dg-test")
expect(composeBefore).toContain("restart: unless-stopped")
expect(composeBefore).toContain(":/home/dev/.docker-git")
Expand Down