Summary
When the worker fetches project-specific skills at workflow start (via ensure_skills), the installed skill files and skills.lock are written into the forge repo's own skills/ directory. These are runtime artifacts that only need to exist long enough to be bind-mounted into the container — they should never touch the repo tree.
Additionally, even though skills were being fetched from Jira project properties successfully, the dynamic (runtime-fetched) skills were not being mounted into the deep agent container. Only skill_0 (default) and skill_1 (project-specific static) were mounted — the fetched skills never made it into the container because resolve_skill_paths did not know about the temp install directory.
Root Cause
The skills fetch pipeline has two stages:
- Clone (
fetcher.py) — clones the skill repo into a tempfile.mkdtemp() directory. This is correctly ephemeral.
- Install (
orchestrator.py) — copies files from the temp clone into skills_dir/{project}/ and writes skills_dir/skills.lock. The temp clone is then deleted.
The problem is in step 2. skills_dir defaults to "skills/" (a relative path in config.py:254), which resolves to the repo's own skills/ directory. The installer copies fetched skills out of the temp dir and into the tracked tree, even though the only downstream consumer (runner.py:185-219) just needs a host path to bind-mount into the container.
A second problem: runner.py called resolve_skill_paths without passing the skills_install_dir, so even when skills were fetched and installed to the temp directory, the resolver only searched the static skills/ tree. The container ended up with only skill_0 and skill_1 mounts — the dynamically fetched skills (e.g. from Jira project properties) were silently missing.
Call Chain
worker.py:243-244 — calls ensure_skills(project_key, jira_client, Path(settings.skills_dir))
orchestrator.py:116 — _install_entry copies from temp clone into skills_dir/{project}/
orchestrator.py:74,141 — reads/writes skills_dir/skills.lock
runner.py:198-215 — _get_skill_mounts resolves skill dirs under skills_dir and bind-mounts them into the container
Impact
- Untracked files pollute the developer's working tree when running the worker locally
- Risk of accidentally committing fetched third-party skill content to the forge repo
skills.lock appears as noise in git status
- Jira-configured skills were fetched but never loaded into the deep agent — the container only received static skills (
skill_0, skill_1), not runtime-fetched ones (skill_2)
Fix
Install fetched skills into a temp directory (following the project convention of tempfile.mkdtemp(prefix="forge-skills-...")) instead of the repo's skills/ directory. The fetched skills only need to live long enough to be bind-mounted into the container. The source-tracked skills/default/ stays in the repo as-is; the resolver just needs to look in two places — the repo for defaults, the temp dir for project overrides.
Pass skills_install_dir through to resolve_skill_paths in the runner so the dynamic skills are included in the container mounts.
Summary
When the worker fetches project-specific skills at workflow start (via
ensure_skills), the installed skill files andskills.lockare written into the forge repo's ownskills/directory. These are runtime artifacts that only need to exist long enough to be bind-mounted into the container — they should never touch the repo tree.Additionally, even though skills were being fetched from Jira project properties successfully, the dynamic (runtime-fetched) skills were not being mounted into the deep agent container. Only
skill_0(default) andskill_1(project-specific static) were mounted — the fetched skills never made it into the container becauseresolve_skill_pathsdid not know about the temp install directory.Root Cause
The skills fetch pipeline has two stages:
fetcher.py) — clones the skill repo into atempfile.mkdtemp()directory. This is correctly ephemeral.orchestrator.py) — copies files from the temp clone intoskills_dir/{project}/and writesskills_dir/skills.lock. The temp clone is then deleted.The problem is in step 2.
skills_dirdefaults to"skills/"(a relative path inconfig.py:254), which resolves to the repo's ownskills/directory. The installer copies fetched skills out of the temp dir and into the tracked tree, even though the only downstream consumer (runner.py:185-219) just needs a host path to bind-mount into the container.A second problem:
runner.pycalledresolve_skill_pathswithout passing theskills_install_dir, so even when skills were fetched and installed to the temp directory, the resolver only searched the staticskills/tree. The container ended up with onlyskill_0andskill_1mounts — the dynamically fetched skills (e.g. from Jira project properties) were silently missing.Call Chain
worker.py:243-244— callsensure_skills(project_key, jira_client, Path(settings.skills_dir))orchestrator.py:116—_install_entrycopies from temp clone intoskills_dir/{project}/orchestrator.py:74,141— reads/writesskills_dir/skills.lockrunner.py:198-215—_get_skill_mountsresolves skill dirs underskills_dirand bind-mounts them into the containerImpact
skills.lockappears as noise ingit statusskill_0,skill_1), not runtime-fetched ones (skill_2)Fix
Install fetched skills into a temp directory (following the project convention of
tempfile.mkdtemp(prefix="forge-skills-...")) instead of the repo'sskills/directory. The fetched skills only need to live long enough to be bind-mounted into the container. The source-trackedskills/default/stays in the repo as-is; the resolver just needs to look in two places — the repo for defaults, the temp dir for project overrides.Pass
skills_install_dirthrough toresolve_skill_pathsin the runner so the dynamic skills are included in the container mounts.