[Forge] Skill bundle auto-discovery - register new SKILL.md without redeploy done

← Forge
Inotify+timer watchdog detects new/drifted/removed bundles, validates frontmatter, hot-reloads registry.

Completion Notes

Auto-completed by supervisor after successful deploy to main

Git Commits (1)

Squash merge: orchestra/task/f79e231a-skill-bundle-auto-discovery-register-new (2 commits) (#734)2026-04-27
Spec File

Effort: deep

Goal

Today scidex/forge/skills_canonical.py only re-scans skills/ at startup or
on a manual python -m scidex.forge.skills_canonical invocation. New
agentskills.io-style bundles dropped in by an external worktree (the scripts/register_kdense_skills.py flow, plus operator-pulled vendor packs
under .claude/skills/<name>/SKILL.md) require a service bounce before
agents can route to them. Build a watchdog + admin endpoint that detects new
or changed SKILL.md bundles, validates the YAML frontmatter, computes bundle_sha, and upserts the row — all without restarting scidex-api.

Acceptance Criteria

☐ Module scidex/forge/skill_autoregister.py with:
scan(roots: list[Path]) -> list[BundleStatus] and
apply(roots: list[Path], dry_run: bool = False) -> dict.
BundleStatus dataclass: slug, path, current_sha, db_sha,
state (new | unchanged | drifted | invalid | removed),
error (optional).
☐ Validation rejects: missing name field, slug ≠ directory name,
metadata.scidex_skill_type not in {tool, persona, bundle},
missing version, body shorter than 200 bytes. Each rejection writes
one row to a new skill_validation_errors(slug, path, error,
detected_at)
audit table.
☐ Watchdog systemd unit scidex-skill-watchdog.service runs every 60s
via a path-based trigger (inotify on skills/, personas/,
.claude/skills/); falls back to a 5-min timer when inotify is
unavailable. Unit + timer added under deploy/bootstrap/systemd/.
☐ Admin endpoint POST /api/forge/skills/rescan (admin-only) returns
{scanned: int, new: int, drifted: int, invalid: int, removed: int}
and refreshes the in-process registry without restart.
Removal handling: if a bundle disappears, the row is soft-marked
frozen = TRUE plus a new retired_at column (added in this task's
migration) — never hard-deleted, because historical
agent_skill_invocations reference skill_name by string.
☐ Tests tests/test_skill_autoregister.py:
(a) brand-new bundle → state=new → row inserted.
(b) edited body → current_shadb_sha → state=drifted
row updated, provenance_commit refreshed.
(c) corrupt YAML → state=invalid → no DB write, error row written.
(d) deleted bundle → state=removedretired_at set, frozen=TRUE.
☐ HTML page /forge/skills/bundles lists all known bundles with
state badges + Rescan button (calls the admin endpoint).
☐ CLI smoke: drop a synthetic bundle into skills/_test_autoreg/,
hit the endpoint, verify the new row, then delete + rescan and
verify retired_at IS NOT NULL.

Approach

  • Read scidex/forge/skills_canonical.py:list_skill_bundles and
  • _parse_frontmatter — reuse them; the autoregister is a thin wrapper
    that adds change-detection + validation + audit.
  • Inotify wrapper via the existing pyinotify dep; degrade to
  • os.scandir polling when inotify is unavailable (e.g., bind-mounted
    FS in containers).
  • Migration migrations/20260428_skill_validation.sql adds
  • skill_validation_errors table and skills.retired_at TIMESTAMPTZ.
  • Watchdog runs as a long-lived service; on every change-event it calls
  • apply(roots) — single-flighted by a file lock so concurrent
    inotify+timer paths cannot collide.
  • Tests fixture writes/deletes bundles in a tmp_path skills root.
  • Dependencies

    • scidex/forge/skills_canonical.py — shared parser.
    • migrations/114_skill_registry_canonical.py — base schema.

    Dependents

    • q-skills-versioning-drift — version-drift logic builds on the
    bundle_sha change-detection this task wires up.
    • q-skills-quality-leaderboard — needs current registry to look up
    bundle author + category metadata.

    Work Log

    2026-04-27 — Slot 0 (Claude MiniMax)

    • Implemented full skill bundle auto-discovery feature per spec
    • Created scidex/forge/skill_autoregister.py with:
    - BundleStatus dataclass (slug, path, current_sha, db_sha, state, error)
    - validate_bundle() enforcing all 5 validation rules + YAML error handling
    - scan(roots) for change detection across skill bundle roots
    - apply(roots, dry_run) for DB persistence (upsert + soft-retire)
    - skill_roots() returning [skills/, personas/, .claude/skills/]
    - SkillWatchdog class (inotify + polling fallback)
    • Added POST /api/forge/skills/rescan admin endpoint to api_routes/forge.py
    • Added GET /forge/skills/bundles HTML page with state badges + Rescan button
    • Created migrations/20260427_skill_validation_errors.py (skill_validation_errors table + skills.retired_at column)
    • Created deploy/bootstrap/systemd/scidex-skill-watchdog.{service,path,timer}
    • Wrote tests/test_skill_autoregister.py — 14 test cases covering all acceptance criteria
    • All tests pass (14/14)
    • Committed 05e23337d and pushed to origin

    Sibling Tasks in Quest (Forge) ↗