docs/backend-migration/plans/2026-04-23-builtin-skill-migration-plan.md
Team mode — coordinator + parallel teammates. Each teammate owns a numbered task on their own branch. Pattern reused from the assistant pilot; lessons captured in
docs/backend-migration/notes/team-operations-playbook.md.Companion specs:
Goal: Move built-in skill resources from the Electron frontend to the Rust backend, embed via include_dir!, rename _builtin/ → auto-inject/, route every consumer through HTTP, and introduce a backend "materialize for gemini" endpoint. Eliminates the same packaging-bug class that H2 fixed for assistants.
Architecture: aionui-extension::skill_service gains include_dir!-embedded BUILTIN_SKILLS. SkillInfo response gains relative_location for builtin source. Two new endpoints (materialize-for-agent / cleanup) manage gemini's filesystem needs. AcpSkillManager swaps fs.readFile for HTTP calls. Frontend resources/skills/ deleted; {cacheDir}/builtin-skills/ cleaned on upgrade.
Tech stack: Rust 2024 + axum + include_dir 0.7 (backend); TypeScript + Electron + Vitest + Playwright (frontend). Two branches, one per repo.
Team size: 1 coordinator + 3 role-teammates (backend-dev, frontend-dev, e2e-tester). No backend-tester / frontend-tester this pilot — scope ≈ 1/3 of assistant pilot, devs self-test their work.
| Branch | Repo | Base | Owner(s) |
|---|---|---|---|
feat/backend-migration-coordinator | AionUi | (existing) | coordinator |
feat/backend-migration-builtin-skills | AionUi | origin/feat/backend-migration-coordinator | frontend-dev, e2e-tester |
feat/builtin-skills | aionui-backend | origin/feat/assistant-user-data | backend-dev |
Coexistence note: aionui-backend branch is based on feat/assistant-user-data (previous pilot, not yet merged to archive). If that branch gets rebased before merge, this branch's commits must be replayed — coordinator handles in T5.
T0 (coordinator setup)
│
▼
T1 (backend-dev: import assets + include_dir + API + materialize + tests + packaging smoke)
│
├──► T2 (frontend-dev: AcpSkillManager HTTP + initAgent materialize + deletes + Vitest)
│ │
│ └──► T3 (e2e-tester: Playwright 8 scenarios)
│ │
│ └──► T4 (coordinator closure)
Critical path: T0 → T1 → T2 → T3 → T4.
No backend-tester: T1 owner writes both unit + HTTP integration tests + runs cargo build --release packaging smoke internally. No frontend-tester: T2 owner writes Vitest alongside code changes.
Owner: coordinator. Depends on: nothing.
git -C /Users/zhoukai/Documents/github/AionUi fetch origin
git -C /Users/zhoukai/Documents/github/aionui-backend fetch origin
cd /Users/zhoukai/Documents/github/AionUi
git checkout -b feat/backend-migration-builtin-skills origin/feat/backend-migration-coordinator
git push -u origin feat/backend-migration-builtin-skills
cd /Users/zhoukai/Documents/github/aionui-backend
git checkout -b feat/builtin-skills origin/feat/assistant-user-data
git push -u origin feat/builtin-skills
TeamCreate team_name="aionui-builtin-skill-migration"
Owner: backend-dev. Branch: feat/builtin-skills (aionui-backend). Depends on: T0.
Working dir: /Users/zhoukai/Documents/github/aionui-backend
TaskUpdate { taskId: "<T1>", status: "in_progress", owner: "backend-dev" }SendMessage to "team-lead": "alive on T1"git rev-parse --abbrev-ref HEAD must be feat/builtin-skillsmkdir -p crates/aionui-app/assets/builtin-skills
cp -R /Users/zhoukai/Documents/github/AionUi/src/process/resources/skills/. \
crates/aionui-app/assets/builtin-skills/
mv crates/aionui-app/assets/builtin-skills/_builtin \
crates/aionui-app/assets/builtin-skills/auto-inject
ls crates/aionui-app/assets/builtin-skills/ shows auto-inject/ + ~19 opt-in subdirs (mermaid, pdf, moltbook, morph-ppt, etc.)feat(skill): import builtin skill corpus from AionUi (rename _builtin → auto-inject)include_dir dependencycrates/aionui-extension/Cargo.toml:
[dependencies]
# ... existing
include_dir = "0.7"
crates/aionui-extension/src/constants.rs:
// before: pub const BUILTIN_AUTO_SKILLS_SUBDIR: &str = "_builtin";
pub const BUILTIN_AUTO_SKILLS_SUBDIR: &str = "auto-inject";
In crates/aionui-extension/src/skill_service.rs, near top:
use include_dir::{Dir, include_dir};
static BUILTIN_SKILLS: Dir<'static> = include_dir!(
"$CARGO_MANIFEST_DIR/../aionui-app/assets/builtin-skills"
);
Change SkillPaths.builtin_skills_dir from PathBuf to Option<PathBuf>; add data_dir: PathBuf field.
Update resolve_skill_paths to accept data_dir param; read AIONUI_BUILTIN_SKILLS_PATH env var into builtin_skills_dir.
Rewrite read_builtin_skill: if env override set, read disk; else BUILTIN_SKILLS.get_file(file_name).and_then(|f| f.contents_utf8()); return empty string on missing; keep validate_filename for path traversal.
Rewrite list_builtin_auto_skills: iterate BUILTIN_SKILLS.get_dir("auto-inject")?.dirs(); for each, parse SKILL.md frontmatter; emit BuiltinAutoSkill { name, description, location: "auto-inject/{name}/SKILL.md" }.
Rewrite list_skills: for source=builtin rows, synthesize location as {data_dir}/builtin-skills-view/{name}/SKILL.md and populate relative_location: Some("auto-inject/{name}/SKILL.md" | "{name}/SKILL.md"). Materialize the view lazily on first read (write embedded content to disk so the export-symlink flow works).
Add new functions materialize_skills_for_agent(paths, conv_id, enabled_skills) -> PathBuf and cleanup_agent_skills(paths, conv_id). Details in backend spec §6.2 and §4.
Add cleanup_orphan_agent_skills<F: Fn(&str) -> bool>(paths, is_live). Scans {data_dir}/agent-skills/*, removes subdirs whose name !is_live(name). Must not import aionui-conversation; predicate is wired in aionui-app.
In crates/aionui-api-types/src/extension.rs (or whichever file holds the skill types):
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BuiltinAutoSkill {
pub name: String,
pub description: String,
pub location: String, // NEW
}
pub struct SkillInfo {
// ... existing fields
#[serde(skip_serializing_if = "Option::is_none")]
pub relative_location: Option<String>, // NEW
}
#[derive(Debug, Clone, Deserialize)]
pub struct MaterializeSkillsRequest {
pub conversation_id: String,
pub enabled_skills: Vec<String>,
}
#[derive(Debug, Clone, Serialize)]
pub struct MaterializeSkillsResponse {
pub dir_path: String,
}
crates/aionui-extension/src/skill_routes.rs, register:
.route("/api/skills/materialize-for-agent", post(materialize_for_agent))
.route("/api/skills/materialize-for-agent/:conversation_id", delete(cleanup_agent_skills))
crates/aionui-app/src/lib.rs, update the resolve_skill_paths call to pass data_dir:
let skill_paths = aionui_extension::resolve_skill_paths(&app_resource_dir, data_dir);
let app_resource_dir = std::env::current_exe()
.ok()
.and_then(|p| p.canonicalize().ok())
.and_then(|p| p.parent().map(|pp| pp.to_path_buf()))
.unwrap_or_else(|| std::path::PathBuf::from("."));
cleanup_orphan_agent_skills(&skill_paths, |id| conv_repo.exists_blocking(id)).await.ok(). Log but don't block on errors.crates/aionui-extension/src/skill_service.rs #[cfg(test)] module, add tests per backend spec §9.1 (13 listed cases). Use AIONUI_BUILTIN_SKILLS_PATH env + tempdir for disk-source tests; use embedded for others.crates/aionui-app/tests/skills_builtin_e2e.rs. Test every endpoint per backend spec §9.2. Follow assistants_e2e.rs pattern (tower::oneshot + init_database_memory + ephemeral data_dir).cargo fmt --all -- --check cleancargo clippy --workspace -- -D warnings cleancargo test --workspace all greencargo test --test skills_builtin_e2e green (new test file)cargo test --test assistants_e2e green (regression — assistant pilot)cargo build --releasetarget/release/aionui-backend --local --port 25901 --data-dir /tmp/skill-smokecurl -s http://127.0.0.1:25901/api/skills/builtin-auto | jq '. | length' must return ≥ 4curl -s -X POST http://127.0.0.1:25901/api/skills/builtin-skill -H 'Content-Type: application/json' -d '{"fileName":"auto-inject/cron/SKILL.md"}' | head -c 200 must return non-empty frontmattercurl -s -X POST http://127.0.0.1:25901/api/skills/materialize-for-agent -H 'Content-Type: application/json' -d '{"conversationId":"smoke","enabledSkills":["mermaid"]}' | jq .dirPath must return absolute pathgit add -A && git commit -m "feat(skill): embed builtin skills via include_dir + materialize-for-agent endpoint"git pushcargo build (debug) to refresh ~/.cargo/bin/aionui-backend symlinkSendMessage team-lead: "T1 complete at SHA <X>. Frontend unblocked on T2."TaskUpdate { taskId: "<T1>", status: "completed" }Owner: frontend-dev. Branch: feat/backend-migration-builtin-skills (AionUi). Depends on: T1.
Working dir: /Users/zhoukai/Documents/github/AionUi
TaskUpdate { taskId: "<T2>", status: "in_progress", owner: "frontend-dev" }SendMessage team-lead: "alive on T2"git pull origin feat/backend-migration-builtin-skillscd /Users/zhoukai/Documents/github/aionui-backend && git checkout feat/builtin-skills && git pull && cargo build — refresh symlinked binary so Electron gets the new backend on next start.git rm -r src/process/resources/skillsrefactor(skill): drop local builtin skills (moved to backend) In src/common/adapter/ipcBridge.ts, add:
materializeSkillsForAgent: httpPost<
{ dirPath: string },
{ conversationId: string; enabledSkills: string[] }
>('/api/skills/materialize-for-agent'),
cleanupSkillsForAgent: httpDelete<void, { conversationId: string }>(
({ conversationId }) => `/api/skills/materialize-for-agent/${encodeURIComponent(conversationId)}`,
),
src/process/task/AcpSkillManager.ts:
autoSkillsDir and skillsDir instance fields and all referencesdiscoverAutoSkills: const list = await ipcBridge.fs.listBuiltinAutoSkills.invoke(); for each {name, description, location}, construct SkillDefinition with location string kept for later lazy body fetch.loadSkillByName: if skill has a location and body not loaded yet, await ipcBridge.fs.readBuiltinSkill.invoke({ fileName: skill.location }); cache body.src/process/utils/initAgent.ts:
getBuiltinSkillsCopyDir, getAutoSkillsDirconst { dirPath } = await ipcBridge.fs.materializeSkillsForAgent.invoke({ conversationId, enabledSkills });dirPath to caller (gemini manager) via existing parameter shapesrc/process/task/agentUtils.ts: delete the getBuiltinSkillsCopyDir import and its usage at L137. Skills already materialized by backend.src/process/agent/gemini/cli/config.ts: delete the L125 path.join(skillsDir, '_builtin') read (semantically wrong; obsolete).src/process/task/GeminiAgentManager.ts: ensure on conversation teardown ipcBridge.fs.cleanupSkillsForAgent.invoke({ conversationId }) is called (fire-and-forget, catch errors).src/process/utils/initStorage.ts:
getBuiltinSkillsCopyDir, getAutoSkillsDirSTORAGE_PATH.builtinSkillsensureAssistantDirs-style block for builtin-skills)const legacyDir = path.join(cacheDir, 'builtin-skills');
if (existsSync(legacyDir)) {
fs.rm(legacyDir, { recursive: true, force: true })
.then(() => console.log('[AionUi] Cleaned up legacy builtin-skills cache'))
.catch(() => {});
}
src/common/types/acpTypes.ts L296-299: change _builtin/ → auto-inject/ in the two JSDoc lines.src/renderer/pages/settings/SkillsHubSettings.tsx: if needed, type-extend SkillInfo with optional relativeLocation. No runtime change.grep -rnE '"_builtin"|/_builtin|_builtin/' src/ must return zero production hits (non-test, non-doc-archival).tests/unit/acpSkillManager.test.ts:
ipcBridge.fs.listBuiltinAutoSkills and readBuiltinSkill.tests/unit/initAgent.materialize.test.ts:
materializeSkillsForAgent.invoke returning { dirPath: '/tmp/x' }.cleanupSkillsForAgent is called on conversation end.bunx tsc --noEmit cleanbun run lint --quiet — baseline unchanged (no new warnings)bun run test --run — no new failures beyond current baselinegit add -A && git commit -m "refactor(skill): route AcpSkillManager through backend HTTP; delete local resource sync"git pushSendMessage team-lead: "T2 complete at SHA <X>. e2e-tester unblocked."TaskUpdate { taskId: "<T2>", status: "completed" }Owner: e2e-tester. Branch: feat/backend-migration-builtin-skills. Depends on: T2.
git pull~/.cargo/bin/aionui-backend is the fresh symlink (readlink + stat)bunx electron-vite build — refresh renderer bundletests/e2e/features/builtin-skill-migration/builtin-skill-migration.e2e.ts with 8 scenarios per frontend spec §9 (e2e-tester row).bun run test:e2e tests/e2e/features/builtin-skill-migration/docs/backend-migration/e2e-reports/2026-04-23-builtin-skill-migration.md with per-scenario matrix + probes + verdict.TaskUpdate completed; SendMessage team-lead "T3 clean".Owner: coordinator. Depends on: T3.
cd /Users/zhoukai/Documents/github/AionUi
git checkout feat/backend-migration-coordinator
git pull
git merge origin/feat/backend-migration-builtin-skills --no-edit
git push
cd /Users/zhoukai/Documents/github/AionUi
bun run build # packages Electron
# Open the packaged .app manually; verify: startup OK; GET /api/skills/builtin-auto returns non-empty; create an ACP conversation with a builtin skill → skill gets auto-injected.
docs/backend-migration/handoffs/coordinator-builtin-skill-migration-2026-04-23.md: final SHAs, scope shipped, lessons, followups.docs/backend-migration/modules/assistant.md (or a new modules/builtin-skill.md if separate subject). Record: endpoints added/changed, rename history, migration flag behavior, feature branch SHAs.Per docs/backend-migration/notes/team-operations-playbook.md:
TaskList + inbox tail + git status both repos + file change count.TaskGet + plan read inside the agent, git fetch && git reset --hard origin/<branch> as self-heal._builtin → auto-inject completecargo test --workspace + new skills_builtin_e2e green; clippy cleanresources/skills/ deleted; TSC + lint clean; Vitest baseline unchanged