From e5cfa4840dab87adabbb839bc0fc7afb8e2758eb Mon Sep 17 00:00:00 2001 From: enne2 Date: Thu, 24 Sep 2026 13:06:16 +0200 Subject: [PATCH] feat: guida AGGIUNGERE-SKILL.md, tool skill_sync e linee guida creazione/sync skill nell'iniezione --- docs/AGGIUNGERE-SKILL.md | 37 ++++++++++++++++++++ extensions/skill-hub.ts | 74 ++++++++++++++++++++++++++++++++++++---- 2 files changed, 105 insertions(+), 6 deletions(-) create mode 100644 docs/AGGIUNGERE-SKILL.md diff --git a/docs/AGGIUNGERE-SKILL.md b/docs/AGGIUNGERE-SKILL.md new file mode 100644 index 0000000..3c35703 --- /dev/null +++ b/docs/AGGIUNGERE-SKILL.md @@ -0,0 +1,37 @@ +# Aggiungere una nuova skill al Skill Hub + +Guida operativa per l'agente (e per l'utente). Le skill vivono nel package +`pi-skill-hub` (canonico: git git.enne2.net/enne2/pi-skill-hub). + +## 1. Creare la skill +1. Crea la directory `skills//` dentro il package con: + - `SKILL.md` — frontmatter (name uguale al nome dir, description con + trigger "usa quando…" ed esclusioni "non usare per…") + corpo operativo + (Outcome, Preconditions, Non-negotiable gates, Workflow, Salvataggio, + Verifica/exit criteria, Resource loading) + - `references/*.md` — dettagli tecnici, catalogo errori, dati di configurazione + - `scripts/…` — helper deterministici (niente segreti, niente path + macchina-specifici: quelli vanno in qmem nei project `host-*`) +2. Rispetta i limiti della spec Agent Skills: SKILL.md ≤ ~500 righe, + dettagli in references, descrizione ≤ 1024 caratteri. +3. I contenuti che oggi vivono in qmem NON si duplicano: in qmem restano solo + puntatori (project della skill) ed evidenze di esecuzione. + +## 2. Validare in locale +- Testa la skill sul task reale (o harness); registra l'esito in qmem + (project della skill o `skills//validazioni`). +- `pi -e ` per provarla senza installare. + +## 3. Sincronizzare sul Git remoto +- Usa il tool `skill_sync` (message opzionale): esegue + `pull --ff-only` → `git add skills/` → commit → push su `origin main`. + Su errore (conflitto/rete) riporta il messaggio: NON fare rebase/merge + automatici; chiedere all'utente. +- Le altre macchine ricevono con l'auto-pull all'avvio di sessione, oppure + `pi update --extensions` (o `git -C pull` + `/skill-sync`). + +## 4. Versionamento +- Commit piccoli e descrittivi; tag `v` per cambiamenti di contratto + (macchine che vogliono stabilità installano `@`). +- Modifiche che cambiano regole operative (es. nuove regole vincolanti) vanno + segnalate all'utente prima del push (review umana = promotion gate). \ No newline at end of file diff --git a/extensions/skill-hub.ts b/extensions/skill-hub.ts index f1715f7..0185270 100644 --- a/extensions/skill-hub.ts +++ b/extensions/skill-hub.ts @@ -15,7 +15,7 @@ import { Type } from "typebox"; import { readdirSync, readFileSync, statSync, existsSync } from "node:fs"; import { join, dirname } from "node:path"; import { fileURLToPath } from "node:url"; -import { spawn } from "node:child_process"; +import { spawn, execFile } from "node:child_process"; const SKILLS_DIR = join(dirname(fileURLToPath(import.meta.url)), "..", "skills"); const MAX_CHARS_PER_SKILL = 200_000; @@ -241,6 +241,40 @@ function searchSkills(query: string, limit: number): { entry: SkillEntry; score: // ---------------------------------------------------------------- extension +function gitExec(root: string, args: string[], timeoutMs = 30_000): Promise { + return new Promise((resolve, reject) => { + execFile("git", ["-C", root, ...args], { timeout: timeoutMs, encoding: "utf8" }, (err, stdout, stderr) => { + if (err) reject(new Error(`git ${args.join(" ")} → ${String(stderr || "").trim()} | ${String(stdout || "").trim()}`)); + else resolve(String(stdout || "").trim()); + }); + }); +} + +/** + * Sincronizza le skill sul repo remoto: pull --ff-only → add skills/ → commit + * (solo se ci sono modifiche) → push. Mai rebase/merge automatici. + */ +async function syncSkills(root: string, message?: string): Promise { + if (!existsSync(join(root, ".git"))) { + throw new Error("Il package non è un clone git: sincronizzazione non disponibile."); + } + await gitExec(root, ["pull", "--ff-only", "-q"], 20_000); + await gitExec(root, ["add", "-A"]); + let committed = ""; + try { + committed = await gitExec(root, [ + "-c", "user.name=enne2", "-c", "user.email=enne2@git.enne2.net", + "commit", "-m", message || `skill-hub: aggiornamento skill (${new Date().toISOString().slice(0, 16)})`, + ], 20_000); + } catch (err) { + const msg = String((err as Error).message); + if (!/nothing to commit|no changes added/i.test(msg)) throw err; + committed = "nessuna modifica da committare"; + } + const push = committed.includes("nessuna modifica") ? "push saltato (nessun commit)" : await gitExec(root, ["push", "origin", "main"], 30_000); + return `pull --ff-only OK; ${committed}; push origin main OK` + (push ? ` (${push.slice(0, 120)})` : ""); +} + export default function (pi: ExtensionAPI) { const PACKAGE_ROOT = join(dirname(fileURLToPath(import.meta.url)), ".."); @@ -263,6 +297,7 @@ export default function (pi: ExtensionAPI) { "## Skill Hub (pi-skill-hub)", `Skill condivise installate (${names.length}): ${names.join(", ")}.`, "Se il task richiede una competenza operativa specializzata e nessuna skill nota corrisponde, esegui skill_search prima di improvvisare; carica la skill scelta con /skill: (o leggi il suo SKILL.md per il corpo completo). Le evidenze di esecuzione vanno registrate in qmem.", + "Per creare o modificare una skill: crea/aggiorna skills// nel package (SKILL.md con frontmatter name+description routing, references/ per i dettagli, scripts/ senza segreti né path macchina-specifici; SKILL.md ≤ 500 righe), poi sincronizza col tool skill_sync (pull --ff-only + add/commit/push su origin main; su conflitto ferma e chiedi all'utente). Le altre macchine ricevono col pull automatico all'avvio di sessione o con pi update --extensions. Guida completa: docs/AGGIUNGERE-SKILL.md nel package.", ].join("\n"); return { systemPrompt: `${event.systemPrompt}\n\n${block}` }; }); @@ -300,6 +335,31 @@ export default function (pi: ExtensionAPI) { }, }); + pi.registerTool({ + name: "skill_sync", + label: "Skill sync", + description: + "Sincronizza le skill condivise sul repo Git remoto (git.enne2.net/enne2/pi-skill-hub): pull --ff-only, add/commit di skills/, push su origin main. Usalo DOPO aver creato o modificato una skill (skills//) per distribuirla alle altre macchine. Su conflitto/rete fallisce con messaggio: non fare rebase automatici, chiedi all'utente.", + parameters: Type.Object({ + message: Type.Optional(Type.String({ description: "Messaggio di commit (default: skill-hub: aggiornamento skill " })), + }), + async execute(_id, params) { + try { + const out = await syncSkills(PACKAGE_ROOT, params.message); + const idx = buildIndex(true); + return { + content: [{ type: "text", text: `Skill Hub sincronizzato. ${out}\nSkill indicizzate: ${idx.entries.map((e) => e.name).join(", ")}` }], + details: { synced: true, skills: idx.entries.map((e) => e.name) }, + }; + } catch (err) { + return { + content: [{ type: "text", text: `Sync fallita: ${(err as Error).message}\nNon fare rebase/merge automatici: risolvere con l'utente.` }], + details: { synced: false, error: (err as Error).message }, + }; + } + }, + }); + pi.registerTool({ name: "skill_info", label: "Skill info", @@ -332,13 +392,15 @@ export default function (pi: ExtensionAPI) { }); pi.registerCommand("skill-sync", { - description: "Ricostruisce l'indice delle skill del Skill Hub", + description: "Skill Hub: pull remoto + push delle skill + ricostruzione indice", handler: async (_name, ctx) => { const idx = buildIndex(true); - ctx.ui.notify( - `Skill Hub: ${idx.entries.length} skill indicizzate (${idx.entries.map((e) => e.name).join(", ")})`, - "info", - ); + try { + const out = await syncSkills(PACKAGE_ROOT); + ctx.ui.notify(`Skill Hub: ${out} — ${idx.entries.length} skill indicizzate`, "info"); + } catch (err) { + ctx.ui.notify(`Skill Hub: sync fallita (${(err as Error).message})`, "warning"); + } }, }); } \ No newline at end of file