feat: guida AGGIUNGERE-SKILL.md, tool skill_sync e linee guida creazione/sync skill nell'iniezione

This commit is contained in:
2026-09-24 13:06:16 +02:00
parent d549dcdb1d
commit e5cfa4840d
2 changed files with 105 additions and 6 deletions
+37
View File
@@ -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/<nome-kebab-case>/` 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/<nome>/validazioni`).
- `pi -e <package>` 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 <clone> pull` + `/skill-sync`).
## 4. Versionamento
- Commit piccoli e descrittivi; tag `v<maggiore>` per cambiamenti di contratto
(macchine che vogliono stabilità installano `@<tag>`).
- Modifiche che cambiano regole operative (es. nuove regole vincolanti) vanno
segnalate all'utente prima del push (review umana = promotion gate).
+68 -6
View File
@@ -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<string> {
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<string> {
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:<name> (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/<nome-kebab-case>/ 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/<nome>/) 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 <data>" })),
}),
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");
}
},
});
}