From 8e5d328b5476b23fde3d063bbfe7e66f24d1be97 Mon Sep 17 00:00:00 2001 From: skulidropek <66840575+skulidropek@users.noreply.github.com> Date: Fri, 27 Mar 2026 09:31:52 +0000 Subject: [PATCH 1/2] feat: auto-load project codex skills --- .../tests/docker-git/entrypoint-auth.test.ts | 7 +++ packages/lib/src/core/templates-entrypoint.ts | 2 + .../src/core/templates-entrypoint/codex.ts | 47 +++++++++++++++++++ .../src/core/templates-entrypoint/tasks.ts | 4 ++ packages/lib/tests/usecases/apply.test.ts | 4 ++ .../lib/tests/usecases/prepare-files.test.ts | 6 +++ 6 files changed, 70 insertions(+) diff --git a/packages/app/tests/docker-git/entrypoint-auth.test.ts b/packages/app/tests/docker-git/entrypoint-auth.test.ts index d863bcb7..55e73386 100644 --- a/packages/app/tests/docker-git/entrypoint-auth.test.ts +++ b/packages/app/tests/docker-git/entrypoint-auth.test.ts @@ -79,6 +79,13 @@ 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('"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).toContain('docker_git_sync_project_codex_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 aeaea8bb..37fc37ca 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 @@ -51,6 +52,7 @@ export const renderEntrypoint = (config: TemplateConfig): string => renderEntrypointInputRc(config), renderEntrypointZshConfig(), renderEntrypointCodexResumeHint(config), + renderEntrypointProjectCodexSkillsSync(config), 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 a2b3b932..25ca2f22 100644 --- a/packages/lib/src/core/templates-entrypoint/codex.ts +++ b/packages/lib/src/core/templates-entrypoint/codex.ts @@ -121,6 +121,53 @@ 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 agent dirs -> agent-specific Codex dirs. + for spec in \ + "10-root-skills::.skills" \ + "20-agents-skills::.agents/skills" \ + "30-agents-dot-skills::.agents/.skills" \ + "40-claude-skills::.claude/skills" \ + "50-claude-dot-skills::.claude/.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/tasks.ts b/packages/lib/src/core/templates-entrypoint/tasks.ts index f81f8c95..5810bf26 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_sync_project_codex_skills +fi + ${renderAgentLaunch(config)} ) &` diff --git a/packages/lib/tests/usecases/apply.test.ts b/packages/lib/tests/usecases/apply.test.ts index b2f63353..7f3345e4 100644 --- a/packages/lib/tests/usecases/apply.test.ts +++ b/packages/lib/tests/usecases/apply.test.ts @@ -153,6 +153,10 @@ 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_sync_project_codex_skills()") + expect(entrypointAfter).toContain('"20-agents-skills::.agents/skills"') + 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 ec6a23d7..89aab20b 100644 --- a/packages/lib/tests/usecases/prepare-files.test.ts +++ b/packages/lib/tests/usecases/prepare-files.test.ts @@ -175,6 +175,11 @@ 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('"10-root-skills::.skills"') + expect(entrypoint).toContain('"20-agents-skills::.agents/skills"') + expect(entrypoint).toContain('"90-codex-dot-skills::.codex/.skills"') 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 +191,7 @@ 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_sync_project_codex_skills\nfi") expect(composeBefore).toContain("container_name: dg-test") expect(composeBefore).toContain("restart: unless-stopped") expect(composeBefore).toContain(":/home/dev/.docker-git") From 328b9a7cb582de5a916505c4240a5f68bcac3cd3 Mon Sep 17 00:00:00 2001 From: skulidropek <66840575+skulidropek@users.noreply.github.com> Date: Fri, 27 Mar 2026 09:44:08 +0000 Subject: [PATCH 2/2] Respect agent-specific project rules --- .../tests/docker-git/entrypoint-auth.test.ts | 13 +++- packages/lib/src/core/templates-entrypoint.ts | 2 + .../src/core/templates-entrypoint/codex.ts | 4 +- .../templates-entrypoint/project-rules.ts | 59 +++++++++++++++++++ .../src/core/templates-entrypoint/tasks.ts | 2 +- packages/lib/tests/usecases/apply.test.ts | 3 +- .../lib/tests/usecases/prepare-files.test.ts | 8 ++- 7 files changed, 84 insertions(+), 7 deletions(-) create mode 100644 packages/lib/src/core/templates-entrypoint/project-rules.ts diff --git a/packages/app/tests/docker-git/entrypoint-auth.test.ts b/packages/app/tests/docker-git/entrypoint-auth.test.ts index 55e73386..1bc5785d 100644 --- a/packages/app/tests/docker-git/entrypoint-auth.test.ts +++ b/packages/app/tests/docker-git/entrypoint-auth.test.ts @@ -81,11 +81,22 @@ describe("renderEntrypoint auth bridge", () => { 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).toContain('docker_git_sync_project_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 37fc37ca..94c12b3b 100644 --- a/packages/lib/src/core/templates-entrypoint.ts +++ b/packages/lib/src/core/templates-entrypoint.ts @@ -25,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, @@ -53,6 +54,7 @@ export const renderEntrypoint = (config: TemplateConfig): string => 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 25ca2f22..d30b1500 100644 --- a/packages/lib/src/core/templates-entrypoint/codex.ts +++ b/packages/lib/src/core/templates-entrypoint/codex.ts @@ -139,13 +139,11 @@ docker_git_sync_project_codex_skills() { rm -rf "$project_skills_root" mkdir -p "$project_skills_root" - # Priority goes from generic -> shared agent dirs -> agent-specific Codex dirs. + # 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" \ - "40-claude-skills::.claude/skills" \ - "50-claude-dot-skills::.claude/.skills" \ "80-codex-skills::.codex/skills" \ "90-codex-dot-skills::.codex/.skills"; do mount_name="${"$"}{spec%%::*}" 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 00000000..9bd97592 --- /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 5810bf26..c36226cc 100644 --- a/packages/lib/src/core/templates-entrypoint/tasks.ts +++ b/packages/lib/src/core/templates-entrypoint/tasks.ts @@ -222,7 +222,7 @@ ${renderEntrypointAutoUpdate()} ${renderEntrypointClone(config)} if [[ "$CLONE_OK" -eq 1 ]]; then - docker_git_sync_project_codex_skills + 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 7f3345e4..5eba4271 100644 --- a/packages/lib/tests/usecases/apply.test.ts +++ b/packages/lib/tests/usecases/apply.test.ts @@ -154,8 +154,9 @@ describe("applyProjectFiles", () => { expect(composeAfter).toContain('mem_limit: "') const entrypointAfter = yield* _(fs.readFileString(path.join(outDir, "entrypoint.sh"))) - expect(entrypointAfter).toContain("docker_git_sync_project_codex_skills()") + 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%"') diff --git a/packages/lib/tests/usecases/prepare-files.test.ts b/packages/lib/tests/usecases/prepare-files.test.ts index 89aab20b..08a8cae3 100644 --- a/packages/lib/tests/usecases/prepare-files.test.ts +++ b/packages/lib/tests/usecases/prepare-files.test.ts @@ -177,9 +177,13 @@ describe("prepareProjectFiles", () => { 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"') @@ -191,7 +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_sync_project_codex_skills\nfi") + 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")