diff --git a/packages/app/tests/docker-git/entrypoint-auth.test.ts b/packages/app/tests/docker-git/entrypoint-auth.test.ts index d863bcb..1bc5785 100644 --- a/packages/app/tests/docker-git/entrypoint-auth.test.ts +++ b/packages/app/tests/docker-git/entrypoint-auth.test.ts @@ -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. Сам агент обязан выполнять финальную проверку, интеграцию и валидацию результата перед ответом пользователю.\"" ) diff --git a/packages/lib/src/core/templates-entrypoint.ts b/packages/lib/src/core/templates-entrypoint.ts index aeaea8b..94c12b3 100644 --- a/packages/lib/src/core/templates-entrypoint.ts +++ b/packages/lib/src/core/templates-entrypoint.ts @@ -15,6 +15,7 @@ import { renderEntrypointClaudeConfig } from "./templates-entrypoint/claude.js" import { renderEntrypointAgentsNotice, renderEntrypointCodexHome, + renderEntrypointProjectCodexSkillsSync, renderEntrypointCodexResumeHint, renderEntrypointCodexSharedAuth, renderEntrypointMcpPlaywright @@ -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, @@ -51,6 +53,8 @@ export const renderEntrypoint = (config: TemplateConfig): string => renderEntrypointInputRc(config), renderEntrypointZshConfig(), renderEntrypointCodexResumeHint(config), + renderEntrypointProjectCodexSkillsSync(config), + renderEntrypointProjectAgentRules(), renderEntrypointAgentsNotice(config), renderEntrypointDockerSocket(config), renderEntrypointGitConfig(config), diff --git a/packages/lib/src/core/templates-entrypoint/codex.ts b/packages/lib/src/core/templates-entrypoint/codex.ts index a2b3b93..d30b150 100644 --- a/packages/lib/src/core/templates-entrypoint/codex.ts +++ b/packages/lib/src/core/templates-entrypoint/codex.ts @@ -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" diff --git a/packages/lib/src/core/templates-entrypoint/project-rules.ts b/packages/lib/src/core/templates-entrypoint/project-rules.ts new file mode 100644 index 0000000..9bd9759 --- /dev/null +++ b/packages/lib/src/core/templates-entrypoint/project-rules.ts @@ -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 diff --git a/packages/lib/src/core/templates-entrypoint/tasks.ts b/packages/lib/src/core/templates-entrypoint/tasks.ts index f81f8c9..c36226c 100644 --- a/packages/lib/src/core/templates-entrypoint/tasks.ts +++ b/packages/lib/src/core/templates-entrypoint/tasks.ts @@ -221,5 +221,9 @@ ${renderEntrypointAutoUpdate()} ${renderEntrypointClone(config)} +if [[ "$CLONE_OK" -eq 1 ]]; then + docker_git_prepare_active_agent_project_rules +fi + ${renderAgentLaunch(config)} ) &` diff --git a/packages/lib/tests/usecases/apply.test.ts b/packages/lib/tests/usecases/apply.test.ts index b2f6335..5eba427 100644 --- a/packages/lib/tests/usecases/apply.test.ts +++ b/packages/lib/tests/usecases/apply.test.ts @@ -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%"') diff --git a/packages/lib/tests/usecases/prepare-files.test.ts b/packages/lib/tests/usecases/prepare-files.test.ts index ec6a23d..08a8cae 100644 --- a/packages/lib/tests/usecases/prepare-files.test.ts +++ b/packages/lib/tests/usecases/prepare-files.test.ts @@ -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"') @@ -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")