Skip to content

feat(skills): skill lifecycle — import, edit, safety guard, multi-source adapters, bundles, and direct-answer persona skills - #411

Merged
Weegy merged 10 commits into
mainfrom
worktree-feat+skill-lifecycle-wave0
Jul 3, 2026
Merged

feat(skills): skill lifecycle — import, edit, safety guard, multi-source adapters, bundles, and direct-answer persona skills#411
Weegy merged 10 commits into
mainfrom
worktree-feat+skill-lifecycle-wave0

Conversation

@Weegy

@Weegy Weegy commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Closes #397, Closes #391, Closes #396

Summary

Full skill-lifecycle feature bundle for issue #397 / epic #391 / #396, built across 9 waves:

  • Wave 0 — foundation: content_hash/forked_from on skills, atomic store operations
  • Wave 1 — import: POST /skills/import, dedup by content hash, paste/upload modal
  • Wave 2 — lifecycle: fork-on-edit for imported skills, export, Operator Skills registry, shared <SkillEditor>
  • Wave 3 — skill-aware Builder: Skills tab in the conversational builder
  • Wave 4 — onboarding: optional "Bring your skills" card in the dashboard wizard
  • Wave 5 — safety: warn-only heuristic prompt-injection guard (EN+DE), pre-activation
  • Wave 6 — multi-source adapters: ChatGPT/custom-GPT JSON export, Codex AGENTS.md, alongside Claude SKILL.md
  • Wave 7 — skill bundles: named resource files attached to a skill, carried through fork/import
  • Wave 8 — direct-answer persona skills: an Agent (orchestrator) can attach N skills directly as persona candidates (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/failure

Every 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.

main had moved since this branch was cut (per-Agent LLM model selection, navbar/UI-prefs work) — merged in and re-verified all gates; conflicts were in agentGraphStore.ts, agentBuilder.ts routes, and InspectorPanel.tsx (both features touch the Agent inspector), resolved by combining both features' additions rather than picking one side.

Testing

  • Middleware: 3818 tests green (0 failures), including new pg-gated coverage run against a real Postgres container for skill lifecycle + persona-skill CRUD
  • web-ui: tsc / eslint / i18n:check / next build all green
  • Live-verified in a local Docker deploy (fresh data wipe): admin onboarding, skill import with the injection guard firing on real content, fork-on-edit, and the persona-skill attach UI all confirmed working end-to-end

Not included (documented as follow-ups)

  • Runtime consumption of Wave 7 bundle resources (needs snapshot enrichment)
  • Whole-plugin import via signed ZIP/Hub (already a complete, separate existing flow — nothing to build)
  • Builder-draft → agent-graph auto-attach (needs a separate design pass; draft publish and the agent_subagents graph don't currently map cleanly)

Weegy added 10 commits July 1, 2026 12:48
…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
@Weegy
Weegy merged commit ac1f700 into main Jul 3, 2026
7 checks passed
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>
@Weegy
Weegy deleted the worktree-feat+skill-lifecycle-wave0 branch August 14, 2026 06:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment