@dforce2055/dai 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,34 @@
3
3
  Formato basado en [Keep a Changelog](https://keepachangelog.com/). Versionado semver
4
4
  (ver `VERSION`).
5
5
 
6
+ ## [0.7.0] — 2026-07-14
7
+
8
+ **dai es el distribuidor de skills de cualquier stack, sin opinar sobre su contenido.**
9
+
10
+ ### Agregado
11
+ - `dai skills install` — namespace de skills (`dai install` queda como alias). Con
12
+ `--from <git-url|path>[#ref]` instala **skills externas** por-stack (.NET, Java,
13
+ Rust…) desde un repo git (público o privado por SSH) o un path local, convertidas
14
+ para los 3 asistentes (Claude/Cursor/Copilot).
15
+ Self-service, one-off, sin registro; `dai sync` no las toca ([ADR-0013](docs/adr/0013-skills-externas-install-from.md)).
16
+ Valida el contrato mínimo (`SKILL.md` con `name` + `description`) y saltea con aviso
17
+ las malformadas; molde en [`templates/skill.md`](templates/skill.md). No valida el contenido.
18
+
19
+ ## [0.6.0] — 2026-07-12
20
+
21
+ Self-update del CLI y blindaje de autoría del repo.
22
+
23
+ ### Agregado
24
+ - `dai upgrade` (alias `update`): actualiza el CLI global a la última publicada
25
+ (`npm i -g …@latest`), con `--check` y `--dry-run`. No toca el repo: reporta el
26
+ drift del scaffold pero deja el `dai sync` explícito del mantenedor (ADR-0012).
27
+
28
+ ### Interno
29
+ - Guard de autoría: rechaza commits autorados/co-autorados por agentes de IA
30
+ (check de CI `authorship` + hook local + `governance/human-authorship.md`).
31
+ - Eliminado `.mailmap` (ya no cumplía función).
32
+ - **111 tests** (+1: `planUpgrade`).
33
+
6
34
  ## [0.5.0] — 2026-07-10
7
35
 
8
36
  **`archive` en el flujo** ([ADR-0011](docs/adr/0011-archive-gate-de-aprobacion.md)): cerrar el CÓMO
@@ -166,6 +194,8 @@ ClickUp y Jira Cloud.
166
194
  - Tests de las rutas de red (jira/clickup/forge) con `fetch` mockeado. Sin links rotos;
167
195
  `files` de npm sin tests ni secretos.
168
196
 
197
+ [0.7.0]: https://github.com/dforce2055/dai/releases/tag/v0.7.0
198
+ [0.6.0]: https://github.com/dforce2055/dai/releases/tag/v0.6.0
169
199
  [0.5.0]: https://github.com/dforce2055/dai/releases/tag/v0.5.0
170
200
  [0.4.0]: https://github.com/dforce2055/dai/releases/tag/v0.4.0
171
201
  [0.3.1]: https://github.com/dforce2055/dai/releases/tag/v0.3.1
package/CONTRIBUTING.md CHANGED
@@ -16,6 +16,13 @@ git clone <tu-fork> dai && cd dai
16
16
  npm test # 48 tests, sin `npm install` (no hay deps)
17
17
  ```
18
18
 
19
+ Opcional, para que los hooks del repo te avisen antes de commitear (cero
20
+ dependencias, sin husky):
21
+
22
+ ```bash
23
+ git config core.hooksPath .githooks # valida convención de commits + autoría humana
24
+ ```
25
+
19
26
  ## Reglas de oro
20
27
 
21
28
  1. **Cero dependencias de runtime.** El CLI corre en redes cerradas sin `npm install`
@@ -29,6 +36,11 @@ npm test # 48 tests, sin `npm install` (no hay deps)
29
36
  necesita un ADR (ver abajo).
30
37
  4. **Secretos nunca en el repo.** Tokens solo en `.env` (gitignored) o el secret
31
38
  store del CI. git usa **SSH**, las APIs usan **tokens scopeados** (ADR-0007).
39
+ 5. **Autoría humana.** El código de `dai` lo **autora y revisa una persona**. No se
40
+ aceptan commits cuyo *author*, *committer* o `Co-authored-by` sea un agente o bot
41
+ (Cursor, Copilot, Claude, …). Un agente puede ayudarte, pero el cambio lo firmas
42
+ tú: con tu identidad y sin el trailer del agente. Un check de CI lo bloquea en cada
43
+ PR (ver [`governance/human-authorship.md`](governance/human-authorship.md)).
32
44
 
33
45
  ## Flujo (la propia metodología)
34
46
 
package/README.md CHANGED
@@ -49,7 +49,7 @@ deja las skills disponibles:
49
49
 
50
50
  ```bash
51
51
  npm i -g @dforce2055/dai # el CLI
52
- dai install # skills de IA → Claude y Cursor (global por defecto)
52
+ dai skills install # skills de IA → Claude y Cursor (global por defecto)
53
53
  ```
54
54
 
55
55
  Y después, en el chat del asistente, según lo que tengas:
@@ -127,7 +127,7 @@ flowchart TD
127
127
 
128
128
  | # | Fase | Cómo | Quién |
129
129
  |---|---|---|---|
130
- | 1 | **Instalar el CLI** | `npm i -g @dforce2055/dai` (o `npm link` en dev) · `dai --version` | dev |
130
+ | 1 | **Instalar el CLI** | `npm i -g @dforce2055/dai` (o `npm link` en dev) · `dai --version` · actualizar después: `dai upgrade` | dev |
131
131
  | 2 | **Bootstrap del repo** | `dai init` → `.dai` + Claude/Copilot/Cursor + config + PR template | dev/lead |
132
132
  | 3a | **Definir el QUÉ** | `/grill-user-story` (una US) · `/grill-epic` (algo grande) · `/doc-to-backlog` (un doc) — te interrogan hasta una US testeable | PO / analista |
133
133
  | 3b | **Publicar la US** | la skill la sube al tracker vía **MCP**, o con **`dai publish <us.md>`** (crea el issue vía token, sin MCP) → devuelve el key | PO / IA |
@@ -157,8 +157,10 @@ flowchart TD
157
157
  | Comando | Qué hace |
158
158
  |---|---|
159
159
  | `dai init [<repo>]` | scaffolder interactivo del repo. Flags: `--for claude\|copilot\|both\|cursor\|all` (asistente, default `all`) · `--pm md\|jira\|clickup` (tracker) · `--openspec` |
160
- | `dai install [--global \| --local <repo>] [--force] [--dry-run] [--for claude\|cursor\|all]` | instala/actualiza skills de IA en Claude y/o Cursor (`--for all` por defecto). `--force` re-copia aunque ya existan. Ej: `dai install --local . --for cursor --force` · `dai install --global --for all --force` |
160
+ | `dai skills install [--global \| --local <repo>] [--force] [--dry-run] [--for claude\|cursor\|all]` | instala/actualiza las skills de dai en Claude y/o Cursor (`--for all` por defecto). `--force` re-copia. Alias: **`dai install`**. Ej: `dai skills install --local . --for cursor --force` |
161
+ | `dai skills install --from <git-url\|path>[#ref] [--for …]` | instala **skills externas** (por-stack: .NET, Java, …) desde un repo/dir, **convertidas para los 3 asistentes**. Self-service, one-off, sin registro; `dai sync` no las toca. Colisión con una skill de dai → salta ([ADR-0013](docs/adr/0013-skills-externas-install-from.md)). Ej: `dai skills install --from github.com/mi-org/net-skills` |
161
162
  | `dai sync [--dry-run] [--for <asistentes>]` | **refresca** skills, constitución, templates y PR template a la versión del CLI — **aditivo** (no pisa tu `CLAUDE.md`), no toca el `.env` ni OpenSpec. Detecta los asistentes del repo o pasás `--for`. `--dry-run` muestra qué cambiaría ([ADR-0010](docs/adr/0010-versionado-y-upgrade.md)) |
163
+ | `dai upgrade [--check] [--dry-run]` · alias `dai update` | **actualiza el CLI global** a la última publicada (`npm i -g …@latest`) — self-update. **No toca el repo**: reporta el drift del scaffold pero deja el `dai sync` al mantenedor. `--check` solo informa · `--dry-run` muestra el comando ([ADR-0012](docs/adr/0012-upgrade-self-update-del-cli.md)) |
162
164
  | `dai publish <us.md>` | crea la US en el tracker (Jira/ClickUp/md) desde un `.md` y devuelve el key. Es el fallback del MCP para publicar sin el asistente |
163
165
  | `dai link-us <ID> [--us <md>]` | crea branch + `implements.yaml`; sin `--us` trae la US del tracker |
164
166
  | `dai link-us <ID> --resync` | re-estampa el `ac_hash` contra la US viva (tras un ⚠️ de check) |
@@ -173,12 +175,33 @@ flowchart TD
173
175
  | `dai doctor` · `dai docs <dest>` · `dai version` | diagnóstico del entorno (incluye **version-drift** del scaffold) · copiar la doc · versión (`dai version` avisa si tu repo quedó atrás) |
174
176
 
175
177
  > **🆕 Mantené tu repo al día — `dai sync`.** Las skills, la constitución y los templates son un
176
- > *caché derivable* del CLI. Cuando actualizás `dai` (`npm i -g @dforce2055/dai`), **`dai doctor` y
178
+ > *caché derivable* del CLI. Cuando actualizás `dai` (`dai upgrade`), **`dai doctor` y
177
179
  > `dai version` te avisan solos** si tu scaffold quedó atrás — con color y un `⬆️` —, y **`dai sync`**
178
180
  > lo refresca: **aditivo** (conserva tu `CLAUDE.md` propio), sin tocar el `.env` ni OpenSpec. Probá sin
179
181
  > riesgo con `dai sync --dry-run`. El versionado es semver: patch/minor no rompen nada; solo un major
180
182
  > pediría migración. ([ADR-0010](docs/adr/0010-versionado-y-upgrade.md))
181
183
 
184
+ > **🧩 Skills de cualquier stack — `dai skills install --from`.** Además de las skills de
185
+ > dai, cada equipo suma las suyas (por-stack: .NET, Java, Rust…) desde su propio repo:
186
+ > `dai skills install --from github.com/tu-org/net-skills`. dai las **convierte para los 3
187
+ > asistentes** (Claude/Cursor/Copilot) e instala. **dai es el distribuidor de skills de
188
+ > cualquier stack, sin opinar sobre su contenido** — self-service, sin registro; `dai sync`
189
+ > sigue siendo solo de dai. ([ADR-0013](docs/adr/0013-skills-externas-install-from.md))
190
+ >
191
+ > La fuente puede ser **pública, privada (por SSH) o un path local**: dai no hace auth
192
+ > propia, delega en git — **si podés `git clone` el repo, dai instala desde ahí**. Para
193
+ > privados usá la forma SSH (`git@github.com:tu-org/net-skills.git`), consistente con el
194
+ > modelo de auth de dai ([ADR-0007](docs/adr/0007-modelo-de-autenticacion.md)). Público =
195
+ > cero fricción entre equipos/máquinas.
196
+ >
197
+ > **Cómo armar una skill que dai ingiera.** Cada skill es un directorio con un `SKILL.md`
198
+ > en **formato Agent Skills de Claude**: frontmatter con **`name`** y **`description`**
199
+ > (obligatorios) + el cuerpo con las instrucciones. dai **valida ese contrato**: si a un
200
+ > `SKILL.md` le falta `name`/`description`, o un dir no tiene `SKILL.md`, lo **saltea con
201
+ > un warn** (no instala una skill rota). El **contenido** no lo valida — bajo tu criterio.
202
+ > Molde: [`templates/skill.md`](templates/skill.md) · ejemplos reales: las skills de dai
203
+ > en [`skills/`](skills/).
204
+
182
205
  Skills (se invocan en el asistente): `/doc-to-backlog` · `/grill-intent` · `/grill-epic` · `/grill-user-story` · `/link-us` ·
183
206
  `/tdd` · `/dai-review`. Config del tracker (`md`\|`jira`\|`clickup`) y tokens: en `.env` —
184
207
  ver [`.env.example`](.env.example). Auth (SSH + tokens): [ADR-0007](docs/adr/0007-modelo-de-autenticacion.md).
package/VERSION CHANGED
@@ -1 +1 @@
1
- 0.5.0
1
+ 0.7.0
package/cli/dai.mjs CHANGED
@@ -28,8 +28,9 @@ import { parsePrRef, getPR, postComment } from "./lib/forge-api.mjs";
28
28
  import { composePrBody, prTitle, forgeTool } from "./lib/pr.mjs";
29
29
  import { dirsEqual } from "./lib/fsutil.mjs";
30
30
  import { parseFlags, parseAssistants, isAssistantToken } from "./lib/args.mjs";
31
- import { versionDrift } from "./lib/semver.mjs";
32
- import { skillToPrompt, skillToCursor, constitution, constitutionCursorRule, envFor, mergeEnv, upsertBlock, reconcileGitignore } from "./lib/bootstrap.mjs";
31
+ import { versionDrift, planUpgrade } from "./lib/semver.mjs";
32
+ import { parseSource } from "./lib/skills-source.mjs";
33
+ import { skillToPrompt, skillToCursor, validateSkill, constitution, constitutionCursorRule, envFor, mergeEnv, upsertBlock, reconcileGitignore } from "./lib/bootstrap.mjs";
33
34
 
34
35
  const HERE = dirname(fileURLToPath(import.meta.url));
35
36
 
@@ -416,6 +417,7 @@ async function cmdPr(opts) {
416
417
 
417
418
  // ── install: skills → ~/.claude/skills o <repo>/.claude/skills ────────────────
418
419
  async function cmdInstall(opts) {
420
+ if (opts.from !== undefined) return cmdInstallFrom(opts); // skills externas (ADR-0013)
419
421
  const skillsSrc = join(ROOT, "skills");
420
422
  const skills = readdirSync(skillsSrc).filter((n) => statSync(join(skillsSrc, n)).isDirectory());
421
423
  let want;
@@ -489,6 +491,89 @@ async function cmdInstall(opts) {
489
491
  if (rl) rl.close();
490
492
  }
491
493
 
494
+ // ── skills install --from: skills EXTERNAS (por-stack) desde un repo/dir ───────
495
+ // Self-service, one-off, sin registro ni sync (ADR-0013). Convierte cada skill
496
+ // para los 3 asistentes y la instala en el repo. No pisa las skills built-in de
497
+ // dai (colisión → warn + skip). `dai sync` NO las toca: es solo de dai.
498
+ function cmdInstallFrom(opts) {
499
+ if (typeof opts.from !== "string" || !opts.from.trim())
500
+ fail("--from necesita una fuente: un git URL (github.com/org/skills[#ref]) o un path local", 2);
501
+ let want;
502
+ try { want = parseAssistants(typeof opts.for === "string" ? opts.for : "all"); }
503
+ catch (e) { fail(`--for ${e.message}`); }
504
+
505
+ let src;
506
+ try { src = parseSource(opts.from); } catch (e) { fail(`--from ${e.message}`, 2); }
507
+
508
+ // Resolver la fuente a un directorio local.
509
+ let root, tmp = null;
510
+ if (src.type === "git") {
511
+ tmp = mkdtempSync(join(tmpdir(), "dai-skills-"));
512
+ info(`clonando ${src.location}${src.ref ? " @ " + src.ref : ""} …`);
513
+ const args = ["clone", "--depth", "1"];
514
+ if (src.ref) args.push("--branch", src.ref);
515
+ args.push(src.location, tmp);
516
+ try { git(args); }
517
+ catch (e) { rmSync(tmp, { recursive: true, force: true }); fail(`no pude clonar la fuente: ${String(e.message).split("\n")[0]}`, 1); }
518
+ root = tmp;
519
+ } else {
520
+ root = src.location;
521
+ if (!existsSync(root)) fail(`no existe la fuente: ${root}`, 2);
522
+ }
523
+
524
+ const cleanup = () => { if (tmp) rmSync(tmp, { recursive: true, force: true }); };
525
+
526
+ // El dir de skills: <root>/skills, o <root> si ya contiene <name>/SKILL.md.
527
+ const skillsDir = existsSync(join(root, "skills")) ? join(root, "skills") : root;
528
+ let entries;
529
+ try {
530
+ entries = readdirSync(skillsDir).filter((n) => {
531
+ try { return statSync(join(skillsDir, n)).isDirectory(); } catch { return false; }
532
+ });
533
+ } catch { cleanup(); fail(`no pude leer la fuente: ${skillsDir}`, 2); }
534
+ if (entries.length === 0) { cleanup(); fail("la fuente no tiene directorios de skill", 2); }
535
+
536
+ // Destino: repo local (default cwd), o --local <repo> / --global.
537
+ const repo = opts.global ? homedir() : (typeof opts.local === "string" ? opts.local : process.cwd());
538
+ const builtins = new Set(readdirSync(join(ROOT, "skills")));
539
+ const forStr = ["claude", "copilot", "cursor"].filter((a) => want[a]).join("+");
540
+ info(`skills install --from ${opts.from} · ${entries.length} candidata(s) · asistentes: ${forStr}${opts.dryRun ? " [dry-run]" : ""}`);
541
+
542
+ let installed = 0, skipped = 0;
543
+ for (const name of entries) {
544
+ const srcDir = join(skillsDir, name);
545
+ const skillPath = join(srcDir, "SKILL.md");
546
+ // Validación estructural (contrato mínimo, no contenido — ADR-0013).
547
+ if (!existsSync(skillPath)) { warn(`'${name}' no tiene SKILL.md — salto`); skipped++; continue; }
548
+ if (builtins.has(name)) { warn(`'${name}' choca con una skill de dai — salto (renombrala, p.ej. ${name}-<stack>)`); skipped++; continue; }
549
+ const md = readFileSync(skillPath, "utf8");
550
+ const bad = validateSkill(md);
551
+ if (bad) { warn(`'${name}' inválida: ${bad} — salto (ver templates/skill.md)`); skipped++; continue; }
552
+ if (opts.dryRun) { info(`[dry-run] ${name} → ${forStr}`); continue; }
553
+ if (want.claude) {
554
+ const t = join(repo, ".claude", "skills", name);
555
+ rmSync(t, { recursive: true, force: true }); mkdirSync(dirname(t), { recursive: true }); cpSync(srcDir, t, { recursive: true });
556
+ }
557
+ if (want.cursor) {
558
+ const t = join(repo, ".cursor", "skills", name);
559
+ rmSync(t, { recursive: true, force: true }); cpSync(srcDir, t, { recursive: true }); writeFileSync(join(t, "SKILL.md"), skillToCursor(md));
560
+ }
561
+ if (want.copilot) {
562
+ const pdir = join(repo, ".github", "prompts"); mkdirSync(pdir, { recursive: true });
563
+ writeFileSync(join(pdir, `${name}.prompt.md`), skillToPrompt(md));
564
+ }
565
+ ok(name); installed++;
566
+ }
567
+ cleanup();
568
+ if (opts.dryRun) { info("dry-run: nada escrito."); return; }
569
+ process.stdout.write("\n");
570
+ if (installed === 0) {
571
+ fail(`0 skills instaladas${skipped ? ` (${skipped} salteada(s))` : ""} — revisá el formato: cada skill es un dir con SKILL.md y frontmatter (name + description). Molde en templates/skill.md`, 1);
572
+ }
573
+ ok(`${installed} skill(s) externa(s) instalada(s)${skipped ? `, ${skipped} salteada(s)` : ""} desde ${opts.from}`);
574
+ warn("skills externas — bajo tu criterio: dai las convierte e instala, no las vetea ni las trackea. `dai sync` no las toca.");
575
+ }
576
+
492
577
  // ── helpers de prompt interactivo ─────────────────────────────────────────────
493
578
  async function askMenu(rl, title, choices, def) {
494
579
  process.stdout.write(`\n ${title}\n`);
@@ -822,6 +907,43 @@ function reportDrift(repo = process.cwd()) {
822
907
  return status;
823
908
  }
824
909
 
910
+ // ── upgrade: self-update del CLI global (ADR-0012) ────────────────────────────
911
+ // Actualiza el paquete npm global de dai a la última publicada. NO toca el repo:
912
+ // si hay drift del scaffold, solo lo reporta (el `dai sync` queda explícito, del
913
+ // mantenedor). El nombre sale de package.json → robusto si cambia el scope.
914
+ function cmdUpgrade(opts) {
915
+ const name = JSON.parse(readFileSync(join(ROOT, "package.json"), "utf8")).name;
916
+ const currentV = readFileSync(join(ROOT, "VERSION"), "utf8").trim();
917
+ const manual = C.cy(`npm i -g ${name}@latest`);
918
+ info(`dai upgrade — CLI v${currentV} (${name})`);
919
+
920
+ let latestV;
921
+ try {
922
+ latestV = execFileSync(npmBin("npm"), ["view", name, "version"], { encoding: "utf8" }).trim();
923
+ } catch {
924
+ fail(`no pude consultar el registry (¿sin red?). Actualizá a mano: ${manual}`, 1);
925
+ }
926
+
927
+ const plan = planUpgrade(currentV, latestV);
928
+ if (plan.action === "unknown") { warn(`no pude comparar versiones (actual '${currentV}', última '${latestV}')`); return; }
929
+ if (plan.action === "up-to-date") { ok(`ya estás en la última (v${currentV})`); reportDrift(); return; }
930
+ if (plan.action === "ahead") { info(`tu CLI (v${currentV}) es más nuevo que el registry (v${latestV}) — nada que actualizar`); reportDrift(); return; }
931
+
932
+ // plan.action === "upgrade"
933
+ if (opts.check) { process.stdout.write(`${C.b(C.y("⬆️ hay update"))} — v${plan.from} → v${plan.to}. Corré ${C.cy("dai upgrade")}\n`); return; }
934
+ if (opts.dryRun) { info(`[dry-run] correría: npm i -g ${name}@latest (v${plan.from} → v${plan.to})`); return; }
935
+
936
+ info(`actualizando v${plan.from} → v${plan.to} …`);
937
+ try {
938
+ execFileSync(npmBin("npm"), ["install", "-g", `${name}@latest`], { stdio: "inherit" });
939
+ } catch {
940
+ fail(`el install falló. Probá a mano: ${manual}`, 1);
941
+ }
942
+ ok(`CLI actualizado a v${plan.to}`);
943
+ const st = reportDrift();
944
+ if (st === null) process.stdout.write(" (No estás en un repo dai — entrá al repo y, si hace falta, corré `dai sync`.)\n");
945
+ }
946
+
825
947
  // ── doctor: diagnóstico ───────────────────────────────────────────────────────
826
948
  function cmdDoctor() {
827
949
  loadEnv();
@@ -898,9 +1020,15 @@ switch (cmd) {
898
1020
  case "pr": cmdPr(opts).catch((e) => fail(String(e.message))); break;
899
1021
  case "done": cmdDone(opts); break;
900
1022
  case "archive": cmdArchive(pos[0], opts); break;
901
- case "install": cmdInstall(opts).catch((e) => fail(String(e.message))); break;
1023
+ case "install": cmdInstall(opts).catch((e) => fail(String(e.message))); break; // alias de `dai skills install`
1024
+ case "skills":
1025
+ if (pos[0] === "install" || pos[0] === undefined) cmdInstall(opts).catch((e) => fail(String(e.message)));
1026
+ else fail(`subcomando de skills desconocido: '${pos[0]}' (por ahora: install)`, 2);
1027
+ break;
902
1028
  case "init": cmdInit(pos[0], opts).catch((e) => fail(String(e.message))); break;
903
1029
  case "sync": cmdSync(pos[0], opts); break;
1030
+ case "upgrade":
1031
+ case "update": cmdUpgrade(opts); break;
904
1032
  case "docs": cmdDocs(pos[0]); break;
905
1033
  case "doctor": cmdDoctor(); break;
906
1034
  case "version": cmdVersion(); break;
@@ -920,12 +1048,14 @@ switch (cmd) {
920
1048
  " pr [--assignee u] [--base b] [--draft] [--yes] crea TU PR/MR precargada (muestra + confirma)\n" +
921
1049
  " forge comment <ref> --body-file <f> · forge pr <ref> comentar/leer una PR ajena (github/gitlab)\n\n" +
922
1050
  "Instalación:\n" +
923
- " install [--global | --local <repo>] [--force] [--dry-run] [--for <asistentes>] skills Claude/Cursor\n" +
1051
+ " skills install [--global | --local <repo>] [--force] [--dry-run] [--for <asistentes>] instala las skills de dai (alias: `install`)\n" +
1052
+ " skills install --from <git-url|path>[#ref] [--for <asistentes>] instala skills EXTERNAS (por-stack), convertidas para los 3 asistentes (ADR-0013)\n" +
924
1053
  " init [<repo>] scaffolder interactivo del repo (asistente, gestor, OpenSpec)\n" +
925
1054
  " --for <asistentes> claude|copilot|cursor (combinables con coma) · o both|all (default all)\n" +
926
1055
  " ej: --for claude,cursor · --for copilot · --for all\n" +
927
1056
  " --pm md|jira|clickup · --openspec (con flags salteas las preguntas)\n" +
928
1057
  " sync [<repo>] [--dry-run] [--for <asistentes>] refresca skills/constitución/templates a la versión del CLI (aditivo; no toca .env ni OpenSpec)\n" +
1058
+ " upgrade [--check] [--dry-run] (alias: update) actualiza el CLI global a la última (npm i -g …@latest) y avisa si el repo quedó atrasado (ADR-0012)\n" +
929
1059
  " docs <destino> documentación conceptual → <destino>\n" +
930
1060
  " doctor diagnóstico del entorno\n\n" +
931
1061
  " (config: .env — ver .env.example)\n"
@@ -14,6 +14,18 @@ export function parseFrontmatter(md) {
14
14
  return { name, description, body: m[2].trim() };
15
15
  }
16
16
 
17
+ // Valida el contrato MÍNIMO de un SKILL.md para que dai lo ingiera y lo convierta a
18
+ // los 3 asistentes (y para que Claude/Cursor/Copilot lo carguen): frontmatter con
19
+ // `name` y `description`. Devuelve null si está OK, o un string con el motivo.
20
+ // NO valida el contenido de la skill — eso es criterio del equipo (ADR-0013).
21
+ export function validateSkill(md) {
22
+ const { name, description } = parseFrontmatter(md);
23
+ if (!name && !description) return "sin frontmatter (falta name y description)";
24
+ if (!name) return "falta 'name' en el frontmatter";
25
+ if (!description) return "falta 'description' en el frontmatter";
26
+ return null;
27
+ }
28
+
17
29
  // Transforma un SKILL.md (Claude) en un prompt file de Copilot (.prompt.md).
18
30
  // Cambia el frontmatter; el cuerpo (la lógica) es el mismo.
19
31
  export function skillToPrompt(md) {
@@ -28,3 +28,18 @@ export function versionDrift(repoV, cliV) {
28
28
  if (cmp < 0) return "cli-behind";
29
29
  return c.major > r.major ? "major-behind" : "minor-behind";
30
30
  }
31
+
32
+ // Plan de `dai upgrade` (ADR-0012): compara el CLI instalado (currentV) con la
33
+ // última publicada en el registry (latestV). Núcleo puro; el I/O (npm view /
34
+ // npm i -g) vive en el comando.
35
+ // up-to-date → iguales → { action, version }
36
+ // ahead → el CLI local es más nuevo → { action, current, latest }
37
+ // upgrade → hay una versión más nueva → { action, from, to }
38
+ // unknown → alguna versión no parsea → { action }
39
+ export function planUpgrade(currentV, latestV) {
40
+ const cmp = compareVersions(currentV, latestV);
41
+ if (cmp === null) return { action: "unknown" };
42
+ if (cmp === 0) return { action: "up-to-date", version: currentV };
43
+ if (cmp > 0) return { action: "ahead", current: currentV, latest: latestV };
44
+ return { action: "upgrade", from: currentV, to: latestV };
45
+ }
@@ -0,0 +1,26 @@
1
+ // dai · resolución de una fuente de skills externas (`dai skills install --from`).
2
+ // Núcleo puro (sin fs ni red): clasifica el argumento en git URL o path local y
3
+ // separa el ref (`#branch`/`#tag`). Ver ADR-0013.
4
+
5
+ // Devuelve { type: 'git'|'path', location, ref }.
6
+ // git → URL clonable. `host/org/repo` (sin esquema) se normaliza a https://.
7
+ // path → ruta local (relativa o absoluta). El `ref` se ignora aguas abajo.
8
+ // ref → lo que va después de '#', o null.
9
+ export function parseSource(src) {
10
+ const raw = String(src ?? "").trim();
11
+ if (!raw) throw new Error("fuente vacía (pasá un git URL o un path)");
12
+
13
+ // Separar el ref (#branch/tag). El scp de git (git@host:org/repo) no usa '#'.
14
+ let ref = null, loc = raw;
15
+ const hash = raw.lastIndexOf("#");
16
+ if (hash > 0) { ref = raw.slice(hash + 1) || null; loc = raw.slice(0, hash); }
17
+
18
+ // Path local explícito (./ ../ / ~/).
19
+ if (/^(\.\.?\/|\/|~\/)/.test(loc)) return { type: "path", location: loc, ref };
20
+ // git por sintaxis de URL (https, ssh, scp git@host:…).
21
+ if (/^(https?:\/\/|ssh:\/\/|git@|[\w.-]+@)/.test(loc)) return { type: "git", location: loc, ref };
22
+ // host/org/repo (p.ej. github.com/org/skills) → git https.
23
+ if (/^[\w.-]+\.[\w.-]+\/.+/.test(loc)) return { type: "git", location: "https://" + loc, ref };
24
+ // Resto: path local relativo sin ./.
25
+ return { type: "path", location: loc, ref };
26
+ }
@@ -0,0 +1,59 @@
1
+ # ADR-0012 — `dai upgrade` (self-update del CLI)
2
+
3
+ - **Estado:** aceptado
4
+ - **Fecha:** 2026-07-12
5
+ - **Decide:** lead / arquitecto de la metodología
6
+
7
+ ## Contexto
8
+
9
+ dai tiene **dos cosas versionadas e independientes**: el **CLI** (el paquete npm
10
+ global, una copia por dev) y el **scaffold del repo** (`.dai/`, skills, `.dai/VERSION`,
11
+ compartido y commiteado). El drift entre ambos ya se **detecta** (`versionDrift`,
12
+ ADR-0010) y `dai doctor`/`dai version` imprimen el estado.
13
+
14
+ Pero actualizar el **CLI** era **manual**: cuando el detector veía el CLI atrasado,
15
+ imprimía el texto `npm i -g @dforce2055/dai` para que el dev lo copiara y corriera a
16
+ mano. No había comando. Fricción repetida, y un detector que empujaba a algo que no
17
+ existía como acción de dai.
18
+
19
+ Escenario típico: un dev vuelve el lunes con el CLI en v0.2.0 y ya salió v0.5.0.
20
+ Tiene que actualizar su CLI (y después traer el repo con `git pull`, que el
21
+ mantenedor ya sincronizó).
22
+
23
+ ## Decisión
24
+
25
+ Agregamos **`dai upgrade`** (alias `update`): **self-update del CLI global** a la
26
+ última versión publicada en el registry.
27
+
28
+ - **Scope: solo el CLI.** Corre `npm i -g <name>@latest` (el `<name>` sale de
29
+ `package.json`, robusto si cambia el scope). `--check` solo informa; `--dry-run`
30
+ muestra el comando sin correrlo.
31
+ - **NO toca el repo.** Tras actualizar, **reporta** el drift del scaffold (`reportDrift`)
32
+ pero **no corre `dai sync`**. El `sync` queda como acto **explícito del mantenedor**:
33
+ un dev con CLI viejo que sincronizara **degradaría** el `.dai/` del repo, y sincronizar
34
+ desde cada máquina genera commits en conflicto.
35
+ - **El núcleo es puro y testeado** (`planUpgrade` en `lib/semver.mjs`): decide
36
+ `up-to-date | ahead | upgrade`. El I/O (`npm view` / `npm i -g`) es la cáscara del
37
+ comando, no se unit-testea (mismo criterio que el `fetch`).
38
+
39
+ ## Consecuencias
40
+
41
+ - **Más fácil:** un comando cierra el loop de "CLI atrasado" en vez de copiar un hint.
42
+ La separación de roles queda nítida: **`upgrade` = tu CLI** (self-update),
43
+ **`sync` = el repo** (mantenedor, commiteado).
44
+ - **Se acepta pagar:** depende del **registry** (es online; en redes cerradas —ADR-0006—
45
+ falla con mensaje claro y fallback al `npm i -g` manual). Asume **npm** como package
46
+ manager del global (no pnpm/yarn/volta/asdf); si el install falla, deriva al comando
47
+ manual. No auto-sincroniza el repo (a propósito).
48
+
49
+ ## Alternativas consideradas
50
+
51
+ - **Auto-correr `dai sync` tras el upgrade** — descartado: un dev degradaría el repo con
52
+ un CLI viejo y sincronizar desde cada máquina genera conflictos. El `sync` es del
53
+ mantenedor.
54
+ - **Pinnear dai como devDependency por-repo** (sin global → sin drift, alineado en
55
+ `npm install`) — descartado como default: dai se diseñó **global y zero-dep para redes
56
+ cerradas** (ADR-0006), y no todos los repos son npm. Queda como opción del consumidor,
57
+ no del método.
58
+ - **No hacer nada** (dejar el hint manual) — descartado: fricción repetida sobre una
59
+ acción que el propio detector ya recomendaba.
@@ -0,0 +1,56 @@
1
+ # ADR-0013 — `dai skills install --from` (skills externas por-stack)
2
+
3
+ - **Estado:** aceptado
4
+ - **Fecha:** 2026-07-14
5
+ - **Decide:** lead / arquitecto de la metodología
6
+
7
+ ## Contexto
8
+
9
+ dai distribuye **sus** skills (las bundleadas: `grill-*`, `link-us`, `tdd`, …) e
10
+ instala/convierte para Claude, Cursor y Copilot. Pero los equipos tienen skills
11
+ **propias de su stack** (.NET, Java, Rust, …) que hoy no tienen lugar: o las copian
12
+ a mano por repo (duplicación, sin conversión a los 3 asistentes), o las meten en el
13
+ paquete dai — que **rompe el ADN**: dai es agnóstico del stack, opina solo en su
14
+ dominio (trazabilidad/distribución), no sobre *qué* dicen tus skills.
15
+
16
+ Falta una forma de que dai **convierta e instale** skills externas, sin volverse
17
+ dueño ni gatekeeper de ellas.
18
+
19
+ ## Decisión
20
+
21
+ Agregamos **`dai skills install --from <git-url|path>[#ref]`**: instala skills
22
+ **externas** desde un repo/dir (con estructura `skills/<nombre>/SKILL.md`, la misma
23
+ de dai), **convertidas para los 3 asistentes** (Claude copia · Cursor `skillToCursor`
24
+ · Copilot `skillToPrompt` → `.github/prompts/`).
25
+
26
+ - **`dai skills` es el namespace canónico** de las operaciones de skills, consistente
27
+ con `dai forge <verb>`. `dai skills install` (sin `--from`) instala las de dai;
28
+ **`dai install` queda como alias** silencioso (backward-compatible).
29
+ - **Self-service, one-off, sin registro.** No hay autorización central ni persistencia:
30
+ no existe `.dai/sources`, dai **no lleva registro** de qué repos usan skills externas.
31
+ El equipo las suma **bajo su propio criterio**.
32
+ - **`dai sync` NO las toca** — sigue siendo **solo** de las skills de dai. Si el equipo
33
+ actualiza sus skills, re-corre `--from`.
34
+ - **Colisión** con una skill built-in de dai → **warn + skip** (no se pisa la
35
+ metodología; renombran, p. ej. `tdd-dotnet`).
36
+
37
+ ## Consecuencias
38
+
39
+ - **Más fácil:** cada equipo suma sus skills por-stack sin tocar dai ni pedir permiso,
40
+ y las escribe **una vez** (dai las sirve a Claude/Cursor/Copilot). El namespace
41
+ `dai skills` deja lugar a crecer (`skills list`, …) sin comandos sueltos.
42
+ - **Se acepta pagar:** es **one-off** (dai no mantiene esas skills al día; re-corrés
43
+ `--from`). **Sin registro** → dai no sabe qué repos tienen skills externas, a
44
+ propósito: cero gatekeeping, bajo riesgo del equipo. Las fuentes remotas dependen de
45
+ git/red, y la **auth se delega en git** (público sin más; privado por SSH o credential
46
+ helper — dai no autentica): *si podés `git clone` la fuente, dai instala desde ahí*.
47
+
48
+ ## Alternativas consideradas
49
+
50
+ - **Persistido (`.dai/sources`) + integrado a `dai sync`** — descartado: obliga a dai a
51
+ **registrar y mantener** fuentes externas (gatekeeping que el equipo no quiere), y
52
+ acopla `dai sync` —que debe ser solo de dai— a repos ajenos.
53
+ - **Meter las skills de stack en el paquete dai** — descartado: rompe el ADN (dai
54
+ agnóstico del stack).
55
+ - **Comando suelto `dai install --from` sin namespace** — descartado: `dai skills <verb>`
56
+ es más claro y consistente con `dai forge <verb>`; `dai install` queda como alias.
@@ -35,6 +35,10 @@
35
35
 
36
36
  Se dejan pasar sin validar: `Merge …`, `Revert …`, `fixup!`/`squash!` y commits de bots (`🤖…`).
37
37
 
38
+ > Esto es solo la validación de **formato**. La **autoría** es otra regla: en el repo
39
+ > de dai no se aceptan commits autorados ni co-autorados por un agente
40
+ > (ver [`human-authorship.md`](human-authorship.md)).
41
+
38
42
  ## Relación con la trazabilidad
39
43
 
40
44
  El link formal QUÉ↔CÓMO vive en el `implements.yaml` (no en el mensaje del commit). Pero si
@@ -0,0 +1,61 @@
1
+ # Autoría humana — quién firma el código de dai
2
+
3
+ > El código de `dai` lo **autora y revisa una persona**. Un agente puede ayudarte a
4
+ > escribirlo, pero el commit lo firmas tú: con tu identidad, y sin dejar al agente
5
+ > como co-autor. No es una postura sobre cómo trabajas — es una regla de contribución
6
+ > de **este repo**, y un check la blinda, sin depender de que nadie "se acuerde"
7
+ > (mismo espíritu que [`ci-rules.md`](ci-rules.md) y [`commit-convention.md`](commit-convention.md)).
8
+
9
+ ## La regla
10
+
11
+ Ningún commit que entre a `dai` puede tener un **agente o bot** como:
12
+
13
+ - **author** — quien escribió el cambio,
14
+ - **committer** — quien lo registró, ni
15
+ - **`Co-authored-by:`** — un co-autor en el mensaje.
16
+
17
+ Concretamente se rechazan identidades de asistentes de código (Cursor, GitHub
18
+ Copilot, Claude, Devin, Codeium, Windsurf, … y cualquier `…[bot]`). La lista es
19
+ editable en [`scripts/no-agent-authors.sh`](../scripts/no-agent-authors.sh).
20
+
21
+ ## Por qué
22
+
23
+ - **La persona firma ([Art. 5](../docs/MANIFIESTO.md#art-5)).** El código que entra a
24
+ `dai` es una decisión de una persona que lo entiende y se hace responsable. Un
25
+ commit firmado por un agente diluye ese "alguien responde por esto".
26
+ - **No vibe coding ([Art. 7](../docs/MANIFIESTO.md#art-7)).** El aporte no es "lo que
27
+ salió del chat": es un cambio que revisaste, entendiste y hiciste tuyo.
28
+ - **Higiene del historial.** El grafo de *Contributors* del repo se arma con
29
+ author/committer/co-author. Un trailer de agente mete al bot como contribuidor —
30
+ y sacarlo después obliga a reescribir historia.
31
+
32
+ ## Distinción importante (no es scope de la metodología)
33
+
34
+ Esto **no** dice cómo debes usar agentes en *tus* proyectos. La metodología es
35
+ agnóstica de la herramienta de abajo ([Art. 2](../docs/MANIFIESTO.md#art-2)): usa los
36
+ agentes como quieras. Esta regla gobierna **la contribución al repo de dai**, igual
37
+ que el naming de ramas o la convención de commits. Por eso vive en `governance/` y
38
+ **no** se publica como parte de las plantillas de la metodología.
39
+
40
+ ## Cómo cumplirla
41
+
42
+ Si un agente te ayudó con un cambio: **revísalo, apropiate de él y commitealo con tu
43
+ identidad**, sin el trailer `Co-authored-by`. Configura tu identidad una vez:
44
+
45
+ ```bash
46
+ git config user.name "Tu Nombre"
47
+ git config user.email "tu@email"
48
+ ```
49
+
50
+ ## Cómo se blinda
51
+
52
+ Dos capas, un solo script ([`scripts/no-agent-authors.sh`](../scripts/no-agent-authors.sh),
53
+ cero dependencias):
54
+
55
+ | Capa | Qué | Cuándo |
56
+ |---|---|---|
57
+ | **Hook local** | [`.githooks/commit-msg`](../.githooks/commit-msg) valida el commit en curso (`--pending`). | Al commitear, si activaste `git config core.hooksPath .githooks`. Es opt-in y salteable — feedback temprano, no barrera. |
58
+ | **CI (el gate)** | El job `authorship` de [`ci.yml`](../.github/workflows/ci.yml) valida todos los commits del PR (`--range`). | En cada PR. Como *required check* **bloquea el merge**. No es salteable. |
59
+
60
+ El hook avisa temprano; el CI es la barrera real. El commit siempre es de una persona
61
+ antes de que un mantenedor lo firme ([`CONTRIBUTING.md`](../CONTRIBUTING.md)).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dforce2055/dai",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "description": "Metodología de desarrollo asistido por IA — CLI de acciones deterministas (trazabilidad QUÉ↔CÓMO).",
5
5
  "repository": { "type": "git", "url": "git+https://github.com/dforce2055/dai.git" },
6
6
  "homepage": "https://dforce2055.github.io/dai/",
@@ -0,0 +1,45 @@
1
+ <!--
2
+ MOLDE DE SKILL · dai
3
+ ─────────────────────────────────────────────────────────────────
4
+ Formato: Agent Skills de Claude (SKILL.md). dai lo INGIERE y lo convierte a los 3
5
+ asistentes (Claude copia · Cursor `skillToCursor` · Copilot `.prompt.md`), con
6
+ `dai skills install --from <repo|path>` (ADR-0013).
7
+
8
+ Estructura del repo/dir fuente:
9
+ skills/
10
+ <nombre-de-la-skill>/
11
+ SKILL.md ← este archivo (renombrado a SKILL.md, en MAYÚSCULAS)
12
+ <recursos...> ← opcional: scripts, plantillas, refs que la skill use
13
+
14
+ CONTRATO MÍNIMO que dai valida (si falta, la salta con un warn):
15
+ - frontmatter con `name` y `description` (ambos obligatorios, en una línea).
16
+ - `name`: slug en kebab-case, IGUAL al nombre del directorio.
17
+ - `description`: una frase — con esto el agente decide CUÁNDO usar la skill.
18
+
19
+ dai NO valida el CONTENIDO (qué dice la skill, qué hacen sus scripts): eso es
20
+ criterio del equipo. Las instalás bajo tu propio riesgo.
21
+ -->
22
+ ---
23
+ name: mi-skill
24
+ description: Qué hace la skill y cuándo conviene invocarla — concreto y orientado al disparador.
25
+ ---
26
+
27
+ # <Título de la skill>
28
+
29
+ Instrucciones para el agente: qué hacer, paso a paso. El **cuerpo es el mismo** para
30
+ Claude, Cursor y Copilot — dai solo ajusta el frontmatter por asistente.
31
+
32
+ ## Cuándo usarla
33
+
34
+ El disparador concreto (qué pide el usuario, o qué situación la activa).
35
+
36
+ ## Pasos
37
+
38
+ 1. …
39
+ 2. …
40
+ 3. …
41
+
42
+ ## Notas
43
+
44
+ Convenciones del stack, ejemplos, o límites de la skill. Si necesita archivos de
45
+ apoyo, ponelos junto al `SKILL.md` y referencialos por ruta relativa.