feat(skills): skill lifecycle — import, edit, safety guard, multi-source adapters, bundles, and direct-answer persona skills - #411
Merged
Conversation
…venance) Groundwork for skill import (#391) and view/edit lifecycle (#397). Backend only, no UI. - migration 0004: add skills.content_hash + skills.forked_from (+ indexes) - computeSkillHash(): deterministic sha256 over canonicalized frontmatter+body; canonicalization mirrors JSON.stringify (recursively sorted keys, drops undefined-valued keys) so the hashed bytes always match the stored jsonb - AgentGraphStore: upsertSkill derives+stores content_hash; updateSkill is now an atomic read-modify-write (SELECT ... FOR UPDATE) that recomputes the hash from effective body+frontmatter; new getSkill() and listSubAgentsBySkillId() - routes: POST /skills forwards frontmatter/source/sourcePath (boundary validated); PATCH /skills/:id gains the same frontmatter guard; new GET /skills/:id (usedByCount, UUID-guarded); skillNode DTO exposes provenance - parseSkillMarkdown() extracted from skillLoader (fs-decoupled, reusable for pasted/uploaded skill text) Tests: hash unit tests, fs-decoupled parser tests, and pg store tests (content-hash derivation, jsonb round-trip stability, cross-path convergence, source immutability). Gates green: build/tsc, eslint, tests — pg suite verified against a real Postgres.
Import a SKILL.md (paste or file) into the registry as a source:'file' skill,
with dedup/convergence and a preview.
Backend:
- skillImport service: slugify, normalizeSkillMarkdown (via parseSkillMarkdown),
importSkillMarkdown(store, req, {dryRun}). Dedup: identical content already
imported (hash match scoped to source:'file') -> unchanged; a newer version of
the SAME origin file (sourcePath match) -> updated in place; otherwise created,
disambiguating the slug against any existing skill so a slug clash never
clobbers another skill.
- store: getSkillByContentHash (optional source scope), getSkillBySlug, and a
race-safe insertSkill (INSERT ... ON CONFLICT DO NOTHING) used by the create
path so an import can never overwrite an existing db/file skill.
- route POST /skills/import ({raw, sourcePath?, dryRun?}); only SKILL.md text is
ingested (bundled code stays on the signed plugin path).
- parseSkillMarkdown normalizes CRLF so Windows-authored files parse + hash like
LF files.
Frontend:
- api client: importSkill / previewImportSkill / getSkill (+ SkillNode gains
frontmatter/sourcePath/contentHash/forkedFrom).
- SkillImportModal (paste + file -> dry-run preview -> confirm), mounted via an
Import button in the admin builder palette, wired to the canvas reload.
- i18n en+de under admin.builder.import / palette.importSkill.
Double-reviewed (Forge/GPT-5.4): folded 2 HIGH (slug-collision overwrite,
TOCTOU db clobber) + 2 MED (CRLF parse, cross-source dedup). Gates green:
middleware build/tsc/eslint + 48 tests (unit + pg vs real Postgres);
web-ui i18n/tsc/eslint/next build.
…edit, export) (#397) One consistent skill editor everywhere, a central registry, and imported skills that are no longer frozen. Backend: - store forkSkill(id): atomically forks an imported (source:'file') skill into an editable source:'db' copy (forked_from + source_path preserved) and migrates every sub-agent reference to the fork. Idempotent — a second fork of the same import returns the existing fork instead of minting a duplicate. - serializeSkillMarkdown: inverse of parseSkillMarkdown; splitFrontmatter now unquotes JSON-quoted values so export -> reimport is an exact round-trip even for values with colons/newlines. - routes: POST /skills/:id/fork, GET /skills/:id/export (sanitized filename). Frontend: - shared <SkillEditor> (extracted from the node-graph InspectorPanel and reused by the registry): editing an imported skill forks-on-first-save then patches the fork (tracked so repeat saves don't re-fork); export downloads SKILL.md. - new Operator /operator/skills registry: list/search/select, used-by-N, import (reused modal), delete with a used-by-aware confirm. Nav entry + i18n. Double-reviewed (Forge/GPT-5.4): folded 1 HIGH (serialize/parse round-trip) + 2 MED (silent delete-orphan -> confirm, duplicate forks -> idempotent fork + editor retarget) + filename sanitize. Gates green: middleware build/tsc/eslint + 44 tests (unit + pg vs real Postgres); web-ui i18n/tsc/eslint/next build.
Give the conversational Builder a first-class skill surface (it had none), so a skill is viewed/edited/imported as a registry skill rather than pasted into a size-limited spec slot. - Extract the registry into a shared, self-loading <SkillsRegistry> (renamed from the operator SkillsDashboard): `initial?` for SSR first paint, otherwise fetches on mount (guarded, and skipped when SSR already provided a list). - Operator /operator/skills now renders the shared component. - New "Skills" tab in the Builder Workspace renders <SkillsRegistry showScopeHint> — list/search/import/view/edit/export, reusing the same editor as the node-graph inspector. A scope caption clarifies it manages the shared registry (attachment to the agent stays via slots). Wired into EditorTab / TAB_KEY / editorWarnings / the tab strip + i18n (builder.workspace.tabSkills). Double-reviewed (Forge/GPT-5.4): no HIGH/MED; folded both LOW notes (skip redundant operator refetch; add builder-tab scope caption). Gates green: web-ui i18n/tsc/eslint/next build (no backend change this wave).
Optional, skippable "Bring your skills" card in the dashboard onboarding wizard (shown in the case-picker step, never a gate). Reuses the Wave-1 import path: an "Import a skill" button opens SkillImportModal; on import it shows the imported name with links to the Skills registry and the Builder (whose Skills tab surfaces the same registry), plus an "import another" affordance for the plural intent. i18n en+de under dashboard.onboarding.bringSkills. Double-reviewed (Forge/GPT-5.4): no HIGH/MED; folded LOW (import-another). Remaining LOW noted as roadmap (deep-link to a fresh draft's skills tab for the sharpest fast-path). Gates green: web-ui i18n/tsc/eslint/next build.
… decision Imported skill content is third-party text; surface likely prompt-injection / instruction-smuggling before a human confirms the import. - skillGuard.scanSkillForRisks(frontmatter, body): heuristic, warn-only pre-activation scan (5 codes: instruction_override, system_prompt_reference, tool_coercion, data_exfiltration, hidden_content). Patterns cover English AND German (omadia is German-facing), so "ignoriere alle vorherigen Anweisungen" is caught too. All quantifiers upper-bounded — no catastrophic backtracking (verified at 10MB). One risk per code with a trimmed excerpt. Never blocks: the preview-then-confirm flow keeps a human in the loop; a hard block would belong at attach/run time, not import. - importSkillMarkdown returns `risks` on every outcome; SkillImportModal renders them in the preview (warning box) with en+de labels. - Versioning decision recorded on computeSkillHash: content-hash identity is v1; explicit version history / rollback deferred until there is demand. Double-reviewed (Forge/GPT-5.4): no HIGH/MED (ReDoS empirically disproven); folded the one substantive finding — added German keyword variants so the guard covers the actual user base's language. Gates green: middleware tsc/eslint + 41 skill tests; web-ui i18n/tsc/eslint/next build.
… Codex AGENTS.md) (#391) Grow the single import front door from Claude-SKILL.md-only to per-source adapters, so a skill can be brought from more of the AI tools users already use. - detectAndNormalize(req) → {skill, format}: (1) a ChatGPT / custom-GPT JSON export (instructions|system_prompt|systemPrompt|prompt becomes the body, name/description from the JSON); (2) a Claude SKILL.md (YAML frontmatter); (3) a Codex AGENTS.md / plain instruction markdown (whole text is the body, name from the first H1). importSkillMarkdown routes through it — dedup, content-hash, the import guard, fork and export all work unchanged across formats. - Frontend: SkillImportModal accepts .json/.txt and the hint names the three sources (en+de). Double-reviewed (Forge/GPT-5.4): no HIGH, no injection bypass; folded the MED — detection now normalizes + trims once and routes every branch off the same string, so a leading newline/BOM on a pasted Claude skill can't misroute to the agents-md adapter (which would leak the frontmatter fence into the prompt and produce a duplicate row). Two LOW (raw-JSON-as-body when no instructions field; H1-anywhere naming) left as preview-mitigated polish. Gates green: middleware tsc/eslint + 48 skill tests; web-ui i18n/tsc/eslint/next build. Roadmap (not built): skill bundles (body + bundled resource files loaded on demand) and whole plugins via the signed-ZIP + Hub path — the code-carrying route stays gated to signed plugins, never skill import.
…agement) (#391) Let a skill carry bundled reference files beyond its body (Claude skills ship a folder of resources). Storage/import/management now; runtime load-on-demand for sub-agents is a deliberate, separately-validated next step. - migration 0005: skill_resources (skill_id FK ON DELETE CASCADE, name, content, UNIQUE(skill_id,name)). - store: listSkillResources, replaceSkillResources (atomic delete-then-insert), and forkSkill now copies the origin's resources to the fork. - import: SkillImportRequest.resources (optional); persisted on create/update AND on an unchanged body (content_hash excludes resources), sent === replace, empty array === clear, deduped by name; SkillImportResult.resourceCount. - routes: POST /skills/import validates resources (safe non-path names); new GET /skills/:id/resources (UUID-guarded, ?names=1 metadata mode). - web-ui: api listSkillResources (names-only by default) + SkillResource; SkillsRegistry lists a skill's resources; i18n en+de. Double-reviewed (Forge/GPT-5.4): folded 2 MED (fork dropped the bundle; resources couldn't be updated/cleared via re-import) + 3 LOW (dup-name count, GET over-fetch → names mode, resource-name path-traversal guard). Gates green: middleware build/tsc/eslint + 66 skill tests (unit + pg vs real Postgres); web-ui i18n/tsc/eslint/next build. Roadmap still open: runtime injection of bundle resources into the sub-agent (needs snapshot enrichment, validate live); whole plugins stay on the existing signed-ZIP + Hub path (code never via skill import); builder→draft auto-attach.
…rn auto-selection An Agent (orchestrator) can now attach N skills directly as "direct-answer" persona candidates via a new agent_persona_skills join table — distinct from agent_subagents.skill_id, which backs a delegated specialist reached via tool-call. A persona skill instead becomes the TOP-LEVEL orchestrator's own system prompt for a turn, with no sub-agent hop. Runtime selection mirrors the existing per-turn model-routing pattern (modelRouter.ts): a cheap Haiku classifier (new personaRouter.ts) picks at most one candidate per turn from just its name+description (progressive disclosure — body never reaches the classifier), runs in parallel with the existing model-routing classifier call, and always falls back to the Agent's default identity on any ambiguity or failure — a persona never blocks a turn. Zero candidates short-circuits before any classifier call. Backend: - migrations/0006_agent_persona_skills.sql — join table, cascades both ways - agentGraphStore: addPersonaSkill/removePersonaSkill/listPersonaSkills/ listAllPersonaSkillLinks/listAgentsByPersonaSkillId - applyDiff.ts: graphSignature folds persona-skill links + referenced skill bodies into the rebuild fingerprint; buildForAgent gained a personaSkills param; registry/index.ts resolves it per-Agent via a new GraphIndex.personaSkillsByAgent - orchestrator.ts: resolveTurnPersona() resolved alongside resolveTurnModel() in both turn paths; getSystemPrompt/composeStableSystemPrompt thread a per-turn persona override; streaming path emits a new turn_persona event (harness-channel-sdk ChatStreamEvent union) - agentBuilder.ts routes: POST/DELETE /agents/:slug/persona-skills (re-runs the Wave 5 heuristic risk guard at attach time, warn-only), GET /skills now returns live risks + usedByAgentsCount, /agents/:slug/graph exposes personaSkillIds + persona_skill edges Frontend: InspectorPanel gains a persona attach/remove chip picker (risk badges, tooltips, slug disambiguation) on the Agent editor; chat page gains a PersonaBadge next to the existing model-routing TriageBadge; full EN/DE i18n. Forge-reviewed: folded 2 MED + 4 LOW findings — cross-provider classifier fallback now uses the Agent's own model instead of a hardcoded Anthropic id; attached persona chips no longer disappear on a skill-catalog load failure; skill risk state now travels with the bulk skill list instead of only surfacing once at attach time; classifier slug matching tolerates quotes/punctuation; the opt-out sentinel no longer collides with a real skill slug; duplicate skill names disambiguated by slug in the UI. 72 pre-existing + 24 new tests green (personaRouter, buildForAgent wiring, graphSignature rebuild triggers, persona-skill CRUD against real Postgres). web-ui i18n/tsc/eslint/next build green.
…ifecycle-wave0 # Conflicts: # middleware/packages/harness-orchestrator/src/registry/agentGraphStore.ts # middleware/src/routes/agentBuilder.ts # web-ui/app/admin/builder/panels/InspectorPanel.tsx
This was referenced Jul 3, 2026
SecurID
added a commit
to SecurID/omadia
that referenced
this pull request
Jul 6, 2026
Conflicts in web-ui/messages/{en,de}.json: both sides appended keys to
admin.builder — kept our apiError key and upstream's persona block (byte5ai#411).
Verified: i18n:check OK (2535 keys), tsc clean, eslint 0 errors, 184/184 tests.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #397, Closes #391, Closes #396
Summary
Full skill-lifecycle feature bundle for issue #397 / epic #391 / #396, built across 9 waves:
content_hash/forked_fromonskills, atomic store operationsPOST /skills/import, dedup by content hash, paste/upload modal<SkillEditor>agent_persona_skills); a per-turn Haiku classifier (twin of the existing model-routing classifier) picks at most one to answer AS the top-level orchestrator, no sub-agent hop, with safe fallback to the Agent's default identity on any ambiguity/failureEvery wave was built with the same cycle: implement → gates (build/tsc/eslint/tests/i18n) → self-review → adversarial review (Forge/GPT-5.4) → fold real findings → re-gate. Wave 8 in particular had 2 MED findings folded (a cross-provider classifier-fallback bug, and an attached-persona-chips-disappear-on-load-error UI bug) plus 4 LOW polish items.
mainhad moved since this branch was cut (per-Agent LLM model selection, navbar/UI-prefs work) — merged in and re-verified all gates; conflicts were inagentGraphStore.ts,agentBuilder.tsroutes, andInspectorPanel.tsx(both features touch the Agent inspector), resolved by combining both features' additions rather than picking one side.Testing
i18n:check/next buildall greenNot included (documented as follow-ups)