@dforce2055/dai 0.1.1 → 0.3.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,56 @@
3
3
  Formato basado en [Keep a Changelog](https://keepachangelog.com/). Versionado semver
4
4
  (ver `VERSION`).
5
5
 
6
+ ## [0.3.0] — 2026-07-08
7
+
8
+ `dai init` ahora es **aditivo**: no pisa la configuración de un repo funcional. Más
9
+ robustez en el parser de `.env` y en `doctor`, y nuevas buenas prácticas agnósticas en
10
+ la constitución.
11
+
12
+ ### Cambiado
13
+ - **`dai init` es aditivo y no destructivo** sobre un repo con config existente:
14
+ - `.env` / `.env.example`: mergea solo las claves de dai que faltan (no reescribe lo del proyecto).
15
+ - `CLAUDE.md` / `copilot-instructions.md`: inserta la constitución como bloque delimitado
16
+ (`<!-- dai:start/end -->`), idempotente — conserva la constitución previa del proyecto.
17
+ - `.gitignore`: reconcilia para versionar skills y comandos (`.claude/skills/`, `.claude/commands/`),
18
+ dejando fuera solo lo personal (`settings.local.json`); quita ignores "broad" (`.claude/`, `CLAUDE.md`)
19
+ que los escondían.
20
+ - **Constitución**: nuevas reglas agnósticas (verificar el comportamiento ≠ que compile, la IA confirma
21
+ antes de construir, docs vivas) + sección "Buenas prácticas (agnósticas)". Tono "tú" neutro.
22
+
23
+ ### Corregido
24
+ - **Parser de `.env`** (`env.mjs`): recorta el comentario inline en valores sin comillas — un token con
25
+ `# ...` al lado ya no rompe el header `Authorization` (error de ByteString).
26
+ - **`dai doctor`**: enumera solo directorios; ya no lista `.DS_Store` como skill.
27
+
28
+ ### Interno
29
+ - **104 tests** (+11 desde 0.2.0: `mergeEnv`, `upsertBlock`, `reconcileGitignore`, parser de `env.mjs`).
30
+ - El sitio (`index.html`, `onboarding.html`) sale del paquete npm (`files[]`) — es capa visual del repo/web.
31
+ - Nueva página de onboarding del dev (`onboarding.html`) en el sitio de GitHub Pages.
32
+
33
+ ## [0.2.0] — 2026-07-08
34
+
35
+ Soporte para **Cursor** como asistente y un `--for` combinable. Incluye la **primera
36
+ contribución de la comunidad** 🎉 (@sermati).
37
+
38
+ ### Agregado
39
+ - **Adaptador de Cursor** ([ADR-0009](docs/adr/0009-adaptador-cursor.md), aporte de la
40
+ comunidad): `dai init` / `install` / `doctor` soportan Cursor — generan
41
+ `.cursor/skills/*/SKILL.md` y `.cursor/rules/dai-constitution.mdc` (regla always-on) desde
42
+ la misma fuente `skills/*/SKILL.md`.
43
+ - **`--for` combinable**: acepta subconjuntos de asistentes (`--for claude,cursor`,
44
+ `--for copilot`), además de `both` (Claude+Copilot) y `all` (los tres). Nuevo
45
+ `parseAssistants()` en `args.mjs`.
46
+
47
+ ### Cambiado
48
+ - **Default de `dai init` = `all`** (Claude + Copilot + Cursor) — deja el repo listo para
49
+ cualquier asistente.
50
+ - El mensaje de "próximos pasos" de `dai init` apunta a la **URL online** de la guía
51
+ `PROBAR.md` (antes una ruta local que el usuario instalado no encontraba).
52
+
53
+ ### Interno
54
+ - **93 tests** (+6 desde 0.1.1: `skillToCursor`, `constitutionCursorRule`, `parseAssistants`).
55
+
6
56
  ## [0.1.1] — 2026-07-06
7
57
 
8
58
  Solo documentación y metadata — **el CLI no cambia** (mismo `ac_hash`, mismo protocolo).
@@ -52,5 +102,7 @@ ClickUp y Jira Cloud.
52
102
  - Tests de las rutas de red (jira/clickup/forge) con `fetch` mockeado. Sin links rotos;
53
103
  `files` de npm sin tests ni secretos.
54
104
 
105
+ [0.3.0]: https://github.com/dforce2055/dai/releases/tag/v0.3.0
106
+ [0.2.0]: https://github.com/dforce2055/dai/releases/tag/v0.2.0
55
107
  [0.1.1]: https://github.com/dforce2055/dai/releases/tag/v0.1.1
56
108
  [0.1.0]: https://github.com/dforce2055/dai/releases/tag/v0.1.0
@@ -11,7 +11,7 @@ apariencia, religión u orientación.
11
11
 
12
12
  Comportamientos que esperamos:
13
13
 
14
- - Lenguaje acogedor e inclusivo.
14
+ - Lenguaje respetuoso y considerado.
15
15
  - Respeto por puntos de vista y experiencias distintas.
16
16
  - Aceptar la crítica constructiva con gracia.
17
17
  - Enfocarse en lo mejor para la comunidad.
package/README.md CHANGED
@@ -28,7 +28,7 @@ escribió, aparece el **vibe coding**, y el *qué* (negocio) se mezcla con el *c
28
28
 
29
29
  Juntas hacen que la IA potencie tu desarrollo **sin perder el control**: todo lo que se
30
30
  construye queda linkeado a por qué se construyó, y **la máquina te avisa sola** cuando algo
31
- se desincroniza. Del solo developer a un equipo entero de desarrollo — el mismo método, con Claude o Copilot.
31
+ se desincroniza. Del solo developer a un equipo entero de desarrollo — el mismo método, con Claude, Copilot o Cursor.
32
32
 
33
33
  ## Cómo funciona (3 ideas)
34
34
 
@@ -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 → tu Claude (Desktop y Code)
52
+ dai 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:
@@ -128,7 +128,7 @@ flowchart TD
128
128
  | # | Fase | Cómo | Quién |
129
129
  |---|---|---|---|
130
130
  | 1 | **Instalar el CLI** | `npm i -g @dforce2055/dai` (o `npm link` en dev) · `dai --version` | dev |
131
- | 2 | **Bootstrap del repo** | `dai init` → `.dai` + Claude/Copilot + config + PR template | dev/lead |
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 |
134
134
  | 4 | **Linkear la US** | `dai link-us <ID>` → branch + `openspec/changes/<id>/implements.yaml` | dev |
@@ -156,8 +156,8 @@ flowchart TD
156
156
 
157
157
  | Comando | Qué hace |
158
158
  |---|---|
159
- | `dai init [<repo>]` | scaffolder interactivo del repo. Flags: `--for claude\|copilot\|both` (asistente, ver arriba) · `--pm md\|jira\|clickup` (tracker) · `--openspec` |
160
- | `dai install [--global \| --local <repo>] [--force] [--dry-run]` | instala/actualiza las skills de IA en Claude. `--force` re-copia aunque ya existan (para **actualizar** tras una nueva versión). Ej: `dai install --local . --force` (este repo) · `dai install --global --force` (tu Claude) |
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` |
161
161
  | `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 |
162
162
  | `dai link-us <ID> [--us <md>]` | crea branch + `implements.yaml`; sin `--us` trae la US del tracker |
163
163
  | `dai link-us <ID> --resync` | re-estampa el `ac_hash` contra la US viva (tras un ⚠️ de check) |
@@ -183,18 +183,23 @@ ver [`.env.example`](.env.example). Auth (SSH + tokens): [ADR-0007](docs/adr/000
183
183
  |---|---|---|
184
184
  | `--for claude` | `.claude/skills/` + `CLAUDE.md` | tu equipo usa **Claude** (Code / Desktop) |
185
185
  | `--for copilot` | `.github/prompts/*.prompt.md` + `.github/copilot-instructions.md` | tu equipo usa **GitHub Copilot** (en VS Code / JetBrains) |
186
- | `--for both` *(default)* | **ambos** | equipo **mixto** (unos con Claude, otros con Copilot) |
187
-
188
- - Es **aditivo, no destructivo**: los dos conjuntos conviven sin pisarse (viven en
186
+ | `--for cursor` | `.cursor/skills/` + `.cursor/rules/dai-constitution.mdc` | tu equipo usa **Cursor Agent** |
187
+ | `--for both` | Claude + Copilot | equipo mixto sin Cursor |
188
+ | `--for all` *(default)* | Claude + Copilot + Cursor | quieres dejar el repo listo para cualquier asistente |
189
+
190
+ - **Combinables:** pasá un subconjunto separado por coma — `--for claude,cursor` o
191
+ `--for copilot`. `all` = los tres · `both` = Claude+Copilot. (Igual en `dai install`,
192
+ aunque ahí Copilot no aplica: no tiene skills instalables.)
193
+ - Es **aditivo, no destructivo**: los conjuntos conviven sin pisarse (viven en
189
194
  carpetas distintas). La **misma** skill se transforma al formato de cada asistente.
190
195
  - Si ejecutas `dai init` sin flag, te lo pregunta de forma interactiva.
191
196
  - **No afecta el CLI:** `dai link-us` / `check` / `stamp` funcionan igual con cualquier
192
197
  `--for` (o ninguno) — el flag solo prepara la **invocación de skills** en el asistente.
193
- - Ante la duda, `--for both`: cubre a todo el equipo y no cuesta nada.
198
+ - Ante la duda, `--for all`: cubre a todo el equipo y no cuesta nada.
194
199
 
195
200
  ## Lo que obtienes en tu repo
196
201
 
197
- Después de `dai init` (con `--for both`):
202
+ Después de `dai init` (con `--for all`):
198
203
 
199
204
  ```
200
205
  mi-repo/
@@ -206,6 +211,9 @@ mi-repo/
206
211
  │ ├── copilot-instructions.md · La constitución, auto-inyectada en cada chat de Copilot
207
212
  │ ├── prompts/*.prompt.md · Las mismas skills, generadas en formato Copilot
208
213
  │ └── pull_request_template.md · Molde de PR/MR atado al link
214
+ ├── .cursor/
215
+ │ ├── skills/ · Las mismas skills, en formato Cursor
216
+ │ └── rules/dai-constitution.mdc · Constitución always-on de Cursor
209
217
  └── .dai/
210
218
  ├── templates/ · formato-us · epica · DoR · DoD · adr · pull-request
211
219
  └── governance/ · branch-naming · ci-rules · commit-convention
@@ -225,6 +233,7 @@ Las skills se invocan en el asistente — **no en todas sus superficies** (lími
225
233
  |---|---|---|
226
234
  | **Claude Desktop** | ✅ | `~/.claude/skills/` (global) — sin IDE ni consola |
227
235
  | **Claude Code** | ✅ | `~/.claude/skills/` + `.claude/skills/` del repo |
236
+ | **Cursor Agent (IDE)** | ✅ | `.cursor/skills/*/SKILL.md` + `.cursor/rules/*.mdc` |
228
237
  | **Copilot en VS Code / JetBrains** | ✅ | los `.github/prompts/*.prompt.md` |
229
238
  | **Copilot CLI** | ✅ | custom agents |
230
239
  | **Copilot app standalone / github.com chat** | ❌ | los prompt files son solo-IDE |
@@ -256,7 +265,7 @@ Además: [`docs/glosario.md`](docs/glosario.md) · guías por rol ([`po`](docs/g
256
265
  dai/
257
266
  ├── cli/ 🖥️ el binario `dai` (Node, cero dependencias) + su suite de tests
258
267
  ├── docs/ 📖 la metodología: MANIFIESTO · METODOLOGIA · SCRUM-CON-IA · EJEMPLO ·
259
- │ glosario · guias/ · detalle/ (10 pasos) · adr/ (0001–0007)
268
+ │ glosario · guias/ · detalle/ (10 pasos) · adr/ (0001–0009)
260
269
  ├── templates/ 🧩 los moldes (formato-us · epica · DoR · DoD · adr · pull-request)
261
270
  ├── skills/ 🤖 doc-to-backlog · grill-intent · grill-epic · grill-user-story · link-us · tdd · dai-review
262
271
  ├── governance/ 🛡️ branch-naming · ci-rules · commit-convention
package/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.1
1
+ 0.3.0
package/cli/dai.mjs CHANGED
@@ -27,8 +27,8 @@ import { branchUrl, commitUrl, parseRemote, detectForge } from "./lib/forge-url.
27
27
  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
- import { parseFlags } from "./lib/args.mjs";
31
- import { skillToPrompt, constitution, envFor } from "./lib/bootstrap.mjs";
30
+ import { parseFlags, parseAssistants } from "./lib/args.mjs";
31
+ import { skillToPrompt, skillToCursor, constitution, constitutionCursorRule, envFor, mergeEnv, upsertBlock, reconcileGitignore } from "./lib/bootstrap.mjs";
32
32
 
33
33
  const HERE = dirname(fileURLToPath(import.meta.url));
34
34
 
@@ -40,7 +40,8 @@ const ok = (m) => process.stdout.write(`✓ ${m}\n`);
40
40
  const info = (m) => process.stdout.write(`› ${m}\n`);
41
41
  const warn = (m) => process.stdout.write(`⚠ ${m}\n`);
42
42
  const ROOT = join(HERE, ".."); // raíz del paquete dai (cli/ está adentro)
43
- const SKILLS_DIR = process.env.CLAUDE_SKILLS_DIR || join(homedir(), ".claude", "skills");
43
+ const CLAUDE_SKILLS_DIR = process.env.CLAUDE_SKILLS_DIR || join(homedir(), ".claude", "skills");
44
+ const CURSOR_SKILLS_DIR = process.env.CURSOR_SKILLS_DIR || join(homedir(), ".cursor", "skills");
44
45
  function git(args, opts = {}) {
45
46
  // stderr en 'pipe' (no 'inherit') para no filtrar errores de git a la salida;
46
47
  // quedan en e.stderr para quien quiera inspeccionarlos.
@@ -412,6 +413,15 @@ async function cmdPr(opts) {
412
413
  async function cmdInstall(opts) {
413
414
  const skillsSrc = join(ROOT, "skills");
414
415
  const skills = readdirSync(skillsSrc).filter((n) => statSync(join(skillsSrc, n)).isDirectory());
416
+ let want;
417
+ try { want = parseAssistants(typeof opts.for === "string" ? opts.for : "all"); }
418
+ catch (e) { fail(`--for ${e.message}`); }
419
+ // Copilot no tiene skills instalables (no hay dir global ni local de skills de Copilot):
420
+ // sus prompts los genera `dai init` en el repo. Se ignora en `install`.
421
+ if (want.copilot) warn("Copilot no tiene skills instalables — se generan con `dai init` en el repo. Ignoro 'copilot'.");
422
+ const wantClaude = want.claude, wantCursor = want.cursor;
423
+ if (!wantClaude && !wantCursor) fail("nada para instalar: pasá --for claude|cursor|all.");
424
+ const installFor = [wantClaude && "claude", wantCursor && "cursor"].filter(Boolean).join("+");
415
425
  const interactive = process.stdin.isTTY && !opts.global && opts.local === undefined;
416
426
  const rl = interactive ? createInterface({ input: process.stdin, output: process.stdout }) : null;
417
427
 
@@ -422,25 +432,54 @@ async function cmdInstall(opts) {
422
432
  return a === "l" ? { scope: "local", repo: process.cwd() } : a === "s" ? { scope: "skip" } : { scope: "global" };
423
433
  };
424
434
 
425
- info(`Instalando skills de dai`);
426
- for (const name of skills) {
427
- const { scope, repo } = await scopeFor(name);
428
- if (scope === "skip") { warn(`salto ${name}`); continue; }
429
- const targetDir = scope === "local" ? join(repo, ".claude", "skills") : SKILLS_DIR;
430
- const src = join(skillsSrc, name), target = join(targetDir, name);
435
+ const installOne = async ({ name, scope, repo, kind }) => {
436
+ const src = join(skillsSrc, name);
437
+ const targetDir = scope === "local"
438
+ ? join(repo, kind === "cursor" ? ".cursor" : ".claude", "skills")
439
+ : kind === "cursor" ? CURSOR_SKILLS_DIR : CLAUDE_SKILLS_DIR;
440
+ const target = join(targetDir, name);
441
+ const label = kind === "cursor" ? `${name} (cursor)` : `${name} (claude)`;
442
+
443
+ let sameAsSource = false;
431
444
  if (existsSync(target)) {
432
- if (dirsEqual(src, target)) { ok(`${name} — ya instalada e idéntica, salto`); continue; }
445
+ if (kind === "claude") {
446
+ sameAsSource = dirsEqual(src, target);
447
+ } else {
448
+ const tempRoot = mkdtempSync(join(tmpdir(), "dai-cursor-skill-"));
449
+ const tempTarget = join(tempRoot, name);
450
+ mkdirSync(tempRoot, { recursive: true });
451
+ cpSync(src, tempTarget, { recursive: true });
452
+ const srcMd = readFileSync(join(src, "SKILL.md"), "utf8");
453
+ writeFileSync(join(tempTarget, "SKILL.md"), skillToCursor(srcMd));
454
+ sameAsSource = dirsEqual(tempTarget, target);
455
+ rmSync(tempRoot, { recursive: true, force: true });
456
+ }
457
+
458
+ if (sameAsSource) { ok(`${label} — ya instalada e idéntica, salto`); return; }
433
459
  if (!opts.force) {
434
- if (!interactive) { warn(`${name} existe y difiere — salto (usa --force)`); continue; }
435
- const a = (await rl.question(` ${name} existe y DIFIERE. ¿[p]isar · [s]altar? (s) `)).trim().toLowerCase();
436
- if (a !== "p") { warn(`salto ${name}`); continue; }
460
+ if (!interactive) { warn(`${label} existe y difiere — salto (usa --force)`); return; }
461
+ const a = (await rl.question(` ${label} existe y DIFIERE. ¿[p]isar · [s]altar? (s) `)).trim().toLowerCase();
462
+ if (a !== "p") { warn(`salto ${label}`); return; }
437
463
  }
438
464
  }
439
- if (opts.dryRun) { info(`[dry-run] ${name} → ${targetDir}`); continue; }
465
+
466
+ if (opts.dryRun) { info(`[dry-run] ${label} → ${targetDir}`); return; }
440
467
  rmSync(target, { recursive: true, force: true });
441
468
  mkdirSync(targetDir, { recursive: true });
442
469
  cpSync(src, target, { recursive: true });
443
- ok(`${name} ${targetDir}`);
470
+ if (kind === "cursor") {
471
+ const srcMd = readFileSync(join(src, "SKILL.md"), "utf8");
472
+ writeFileSync(join(target, "SKILL.md"), skillToCursor(srcMd));
473
+ }
474
+ ok(`${label} → ${targetDir}`);
475
+ };
476
+
477
+ info(`Instalando skills de dai (${installFor})`);
478
+ for (const name of skills) {
479
+ const { scope, repo } = await scopeFor(name);
480
+ if (scope === "skip") { warn(`salto ${name}`); continue; }
481
+ if (wantClaude) await installOne({ name, scope, repo, kind: "claude" });
482
+ if (wantCursor) await installOne({ name, scope, repo, kind: "cursor" });
444
483
  }
445
484
  if (rl) rl.close();
446
485
  }
@@ -461,17 +500,6 @@ async function askYesNo(rl, q, def = false) {
461
500
  return ["s", "si", "sí", "y", "yes"].includes(a);
462
501
  }
463
502
 
464
- // Asegura que ciertas entradas estén en el .gitignore del repo (crea o agrega).
465
- function ensureGitignored(repo, entries) {
466
- const gi = join(repo, ".gitignore");
467
- const cur = existsSync(gi) ? readFileSync(gi, "utf8") : "";
468
- const have = new Set(cur.split(/\r?\n/).map((s) => s.trim()));
469
- const add = entries.filter((e) => !have.has(e));
470
- if (add.length === 0) return false;
471
- writeFileSync(gi, cur + (cur && !cur.endsWith("\n") ? "\n" : "") + "\n# dai — secretos, no commitear\n" + add.join("\n") + "\n");
472
- return true;
473
- }
474
-
475
503
  // ── init: scaffolder interactivo del repo ─────────────────────────────────────
476
504
  async function cmdInit(repo, opts) {
477
505
  repo = repo || "."; // por defecto, el directorio actual (como git init / npm init)
@@ -483,12 +511,15 @@ async function cmdInit(repo, opts) {
483
511
  // Preguntas primero (después cerramos readline para liberar stdin a los instaladores).
484
512
  let forOpt = typeof opts.for === "string" ? opts.for.toLowerCase() : null;
485
513
  if (!forOpt && rl) forOpt = await askMenu(rl, "¿Para qué asistente de IA preparo el repo? (genera las skills en su formato)", [
486
- { value: "both", label: "Claude + Copilot equipo mixto (recomendado ante la duda)" },
514
+ { value: "all", label: "Todos (Claude + Copilot + Cursor) por defecto" },
515
+ { value: "both", label: "Claude + Copilot — equipo mixto (sin Cursor)" },
487
516
  { value: "claude", label: "Solo Claude (Code / Desktop)" },
488
517
  { value: "copilot", label: "Solo Copilot (en VS Code / JetBrains)" },
489
- ], "both");
490
- forOpt = forOpt || "both";
491
- if (!["claude", "copilot", "both"].includes(forOpt)) fail(`--for inválido: '${forOpt}' (claude|copilot|both)`);
518
+ { value: "cursor", label: "Solo Cursor (Agent)" },
519
+ ], "all");
520
+ forOpt = forOpt || "all";
521
+ let want;
522
+ try { want = parseAssistants(forOpt); } catch (e) { fail(`--for ${e.message}`); }
492
523
 
493
524
  let pm = typeof opts.pm === "string" ? opts.pm.toLowerCase() : null;
494
525
  if (!pm && rl) pm = await askMenu(rl, "¿Dónde van a vivir las User Stories?", [
@@ -515,8 +546,7 @@ async function cmdInit(repo, opts) {
515
546
  if (rl) rl.close();
516
547
 
517
548
  // ── Generación ───────────────────────────────────────────────────────────────
518
- const wantClaude = forOpt !== "copilot";
519
- const wantCopilot = forOpt !== "claude";
549
+ const wantClaude = want.claude, wantCopilot = want.copilot, wantCursor = want.cursor;
520
550
  process.stdout.write("\n Configurando…\n\n");
521
551
 
522
552
  const dai = join(repo, ".dai");
@@ -527,11 +557,27 @@ async function cmdInit(repo, opts) {
527
557
  writeFileSync(join(dai, "VERSION"), readFileSync(join(ROOT, "VERSION"), "utf8"));
528
558
  ok(".dai/ moldes (templates) + reglas (governance) del método");
529
559
 
530
- cpSync(join(ROOT, ".env.example"), join(repo, ".env.example"));
531
- const envPath = join(repo, ".env");
532
- if (existsSync(envPath)) warn(".env ya existe — lo dejo como está");
533
- else { writeFileSync(envPath, envFor(pm)); ok(`.env listo, DAI_PM=${pm}${pm === "md" ? "" : " (completa el token)"}`); }
534
- if (ensureGitignored(repo, [".env"])) ok(".gitignore .env agregado (los tokens NO se commitean)");
560
+ // .env.example — aditivo: agrega las claves de dai que falten (no pisa el del proyecto).
561
+ const exSrc = readFileSync(join(ROOT, ".env.example"), "utf8");
562
+ const exPath = join(repo, ".env.example");
563
+ if (existsSync(exPath)) {
564
+ const cur = readFileSync(exPath, "utf8"), merged = mergeEnv(cur, exSrc);
565
+ if (merged !== cur) { writeFileSync(exPath, merged); ok(".env.example claves de dai agregadas (aditivo)"); }
566
+ else ok(".env.example ya tenía la config de dai");
567
+ } else { writeFileSync(exPath, exSrc); ok(".env.example creado"); }
568
+
569
+ // .env — aditivo: agrega las claves de dai que falten; si no existe, lo crea.
570
+ const envPath = join(repo, ".env"), envBlock = envFor(pm);
571
+ if (existsSync(envPath)) {
572
+ const cur = readFileSync(envPath, "utf8"), merged = mergeEnv(cur, envBlock);
573
+ if (merged !== cur) { writeFileSync(envPath, merged); ok(`.env claves de dai agregadas (aditivo, DAI_PM=${pm}${pm === "md" ? "" : " — completa el token"})`); }
574
+ else ok(".env ya tenía la config de dai");
575
+ } else { writeFileSync(envPath, envBlock); ok(`.env listo, DAI_PM=${pm}${pm === "md" ? "" : " (completa el token)"}`); }
576
+
577
+ // .gitignore — versiona los artefactos de dai (según --for), deja fuera solo lo personal.
578
+ const giPath = join(repo, ".gitignore");
579
+ const gi = reconcileGitignore(existsSync(giPath) ? readFileSync(giPath, "utf8") : "", want);
580
+ if (gi.changed) { writeFileSync(giPath, gi.text.endsWith("\n") ? gi.text : gi.text + "\n"); ok(".gitignore ajustado (skills/constitución versionadas; .env y settings.local.json fuera)"); }
535
581
 
536
582
  mkdirSync(join(repo, ".github"), { recursive: true });
537
583
  cpSync(join(ROOT, "templates", "pull-request.md"), join(repo, ".github", "pull_request_template.md"));
@@ -544,15 +590,31 @@ async function cmdInit(repo, opts) {
544
590
  const dir = join(repo, ".claude", "skills");
545
591
  mkdirSync(dir, { recursive: true });
546
592
  for (const name of skills) cpSync(join(skillsSrc, name), join(dir, name), { recursive: true });
547
- writeFileSync(join(repo, "CLAUDE.md"), constitution("claude"));
548
- ok(`Claude: .claude/skills/ (${skills.length}) + CLAUDE.md → conoce el método y las skills`);
593
+ const cPath = join(repo, "CLAUDE.md"), cCur = existsSync(cPath) ? readFileSync(cPath, "utf8") : "";
594
+ writeFileSync(cPath, upsertBlock(cCur, constitution("claude")));
595
+ ok(`Claude: .claude/skills/ (${skills.length}) + CLAUDE.md${cCur ? " (bloque dai, aditivo)" : ""} → conoce el método y las skills`);
549
596
  }
550
597
  if (wantCopilot) {
551
598
  const pdir = join(repo, ".github", "prompts");
552
599
  mkdirSync(pdir, { recursive: true });
553
600
  for (const name of skills) writeFileSync(join(pdir, `${name}.prompt.md`), skillToPrompt(readFileSync(join(skillsSrc, name, "SKILL.md"), "utf8")));
554
- writeFileSync(join(repo, ".github", "copilot-instructions.md"), constitution("copilot"));
555
- ok(`Copilot: .github/prompts/ (${skills.length}) + copilot-instructions.md`);
601
+ const ciPath = join(repo, ".github", "copilot-instructions.md"), ciCur = existsSync(ciPath) ? readFileSync(ciPath, "utf8") : "";
602
+ writeFileSync(ciPath, upsertBlock(ciCur, constitution("copilot")));
603
+ ok(`Copilot: .github/prompts/ (${skills.length}) + copilot-instructions.md${ciCur ? " (bloque dai, aditivo)" : ""}`);
604
+ }
605
+ if (wantCursor) {
606
+ const dir = join(repo, ".cursor", "skills");
607
+ mkdirSync(dir, { recursive: true });
608
+ for (const name of skills) {
609
+ const src = join(skillsSrc, name);
610
+ const target = join(dir, name);
611
+ cpSync(src, target, { recursive: true });
612
+ const srcMd = readFileSync(join(src, "SKILL.md"), "utf8");
613
+ writeFileSync(join(target, "SKILL.md"), skillToCursor(srcMd));
614
+ }
615
+ mkdirSync(join(repo, ".cursor", "rules"), { recursive: true });
616
+ writeFileSync(join(repo, ".cursor", "rules", "dai-constitution.mdc"), constitutionCursorRule());
617
+ ok(`Cursor: .cursor/skills/ (${skills.length}) + .cursor/rules/dai-constitution.mdc`);
556
618
  }
557
619
 
558
620
  // ── OpenSpec ───────────────────────────────────────────────────────────────
@@ -561,7 +623,8 @@ async function cmdInit(repo, opts) {
561
623
  // dejaba a medias porque se intentaba correr su modo interactivo anidado.)
562
624
  process.stdout.write("\n");
563
625
  const openspecPresent = () => { try { execFileSync(npmBin("openspec"), ["--version"], { stdio: "ignore" }); return true; } catch { return false; } };
564
- const osTools = { claude: "claude", copilot: "github-copilot", both: "claude,github-copilot" }[forOpt] || "claude,github-copilot";
626
+ const osTools = [want.claude && "claude", want.copilot && "github-copilot", want.cursor && "cursor"]
627
+ .filter(Boolean).join(",") || "claude,github-copilot,cursor";
565
628
  const osHint = "para sumarlo después: npm i -g @fission-ai/openspec@latest && openspec init --tools " + osTools;
566
629
  if (hasOpenspec) {
567
630
  ok("OpenSpec: ya inicializado en el repo");
@@ -596,7 +659,7 @@ async function cmdInit(repo, opts) {
596
659
  : ` 1. Completa el token de ${pm} en .env, y verifica con: dai doctor\n`);
597
660
  process.stdout.write(" 2. dai link-us <ID> → crea la branch + el link a la US\n");
598
661
  process.stdout.write(" 3. Implementa con test primero, después: dai check\n");
599
- process.stdout.write(" Guía paso a paso: docs/PROBAR.md\n\n");
662
+ process.stdout.write(" Guía paso a paso: https://github.com/dforce2055/dai/blob/main/docs/PROBAR.md\n\n");
600
663
  }
601
664
 
602
665
  // ── docs: documentación conceptual → <destino> ────────────────────────────────
@@ -614,16 +677,34 @@ function cmdDoctor() {
614
677
 
615
678
  // Una skill sirve si está en el repo actual (.claude/skills, la puso `dai init`)
616
679
  // O global (~/.claude/skills, la puso `dai install`). Reportamos dónde.
680
+ const skillsRoot = join(ROOT, "skills");
681
+ const skillNames = readdirSync(skillsRoot).filter((n) => statSync(join(skillsRoot, n)).isDirectory());
617
682
  const localDir = join(process.cwd(), ".claude", "skills");
618
683
  info("skills (repo local / global ~/.claude/skills):");
619
- for (const name of readdirSync(join(ROOT, "skills"))) {
620
- const local = existsSync(join(localDir, name)), global = existsSync(join(SKILLS_DIR, name));
684
+ for (const name of skillNames) {
685
+ const local = existsSync(join(localDir, name)), global = existsSync(join(CLAUDE_SKILLS_DIR, name));
621
686
  if (local && global) ok(`${name} (local + global)`);
622
687
  else if (local) ok(`${name} (local, este repo)`);
623
688
  else if (global) ok(`${name} (global)`);
624
689
  else warn(`${name} — no instalada (dai init en el repo, o dai install global)`);
625
690
  }
626
691
 
692
+ const cursorLocalDir = join(process.cwd(), ".cursor", "skills");
693
+ info("skills Cursor (repo local / global ~/.cursor/skills):");
694
+ for (const name of skillNames) {
695
+ const local = existsSync(join(cursorLocalDir, name)), global = existsSync(join(CURSOR_SKILLS_DIR, name));
696
+ if (local && global) ok(`${name} (local + global)`);
697
+ else if (local) ok(`${name} (local, este repo)`);
698
+ else if (global) ok(`${name} (global)`);
699
+ else warn(`${name} — no instalada en Cursor (dai init --for cursor|all, o dai install --for cursor|all)`);
700
+ }
701
+ const localRule = join(process.cwd(), ".cursor", "rules", "dai-constitution.mdc");
702
+ const globalRule = join(homedir(), ".cursor", "rules", "dai-constitution.mdc");
703
+ if (existsSync(localRule) && existsSync(globalRule)) ok("constitución Cursor (local + global)");
704
+ else if (existsSync(localRule)) ok("constitución Cursor (local)");
705
+ else if (existsSync(globalRule)) ok("constitución Cursor (global)");
706
+ else warn("falta constitución Cursor (dai-constitution.mdc)");
707
+
627
708
  info("adaptador de PM:");
628
709
  const pm = process.env.DAI_PM || "md";
629
710
  ok(`DAI_PM=${pm}`);
@@ -680,9 +761,10 @@ switch (cmd) {
680
761
  " pr [--assignee u] [--base b] [--draft] [--yes] crea TU PR/MR precargada (muestra + confirma)\n" +
681
762
  " forge comment <ref> --body-file <f> · forge pr <ref> comentar/leer una PR ajena (github/gitlab)\n\n" +
682
763
  "Instalación:\n" +
683
- " install [--global | --local <repo>] [--force] [--dry-run] skills → Claude\n" +
764
+ " install [--global | --local <repo>] [--force] [--dry-run] [--for <asistentes>] skills → Claude/Cursor\n" +
684
765
  " init [<repo>] scaffolder interactivo del repo (asistente, gestor, OpenSpec)\n" +
685
- " --for claude|copilot|both para qué asistente preparar el repo (default both; ante duda, both)\n" +
766
+ " --for <asistentes> claude|copilot|cursor (combinables con coma) · o both|all (default all)\n" +
767
+ " ej: --for claude,cursor · --for copilot · --for all\n" +
686
768
  " --pm md|jira|clickup · --openspec (con flags salteas las preguntas)\n" +
687
769
  " docs <destino> documentación conceptual → <destino>\n" +
688
770
  " doctor diagnóstico del entorno\n\n" +
package/cli/lib/args.mjs CHANGED
@@ -5,6 +5,23 @@
5
5
 
6
6
  export const camel = (s) => s.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
7
7
 
8
+ // Parsea el valor de `--for` como una LISTA COMBINABLE de asistentes.
9
+ // Acepta "claude", "copilot", "cursor" (combinables con coma o espacio),
10
+ // "both" (= claude+copilot) y "all" (= los tres). Lanza si hay un token inválido.
11
+ // Ej.: "claude,cursor" → { claude:true, copilot:false, cursor:true }.
12
+ export function parseAssistants(str) {
13
+ const want = { claude: false, copilot: false, cursor: false };
14
+ const toks = String(str ?? "all").toLowerCase().split(/[,\s]+/).filter(Boolean);
15
+ if (toks.length === 0) { want.claude = want.copilot = want.cursor = true; return want; }
16
+ for (const t of toks) {
17
+ if (t === "all") { want.claude = want.copilot = want.cursor = true; }
18
+ else if (t === "both") { want.claude = want.copilot = true; }
19
+ else if (t in want) { want[t] = true; }
20
+ else throw new Error(`asistente inválido: '${t}' (claude|copilot|cursor, combinables con coma; o both|all)`);
21
+ }
22
+ return want;
23
+ }
24
+
8
25
  export function parseFlags(argv) {
9
26
  const opts = {};
10
27
  const pos = [];
@@ -27,6 +27,20 @@ export function skillToPrompt(md) {
27
27
  return `${fm}\n\n${body}\n`;
28
28
  }
29
29
 
30
+ // Transforma un SKILL.md (Claude) en un SKILL.md de Cursor.
31
+ // Conserva name/description/body y ajusta solo el frontmatter.
32
+ export function skillToCursor(md) {
33
+ const { name, description, body } = parseFrontmatter(md);
34
+ if (!name) return md.trim() + "\n";
35
+ const fm = [
36
+ "---",
37
+ `name: ${name}`,
38
+ `description: ${JSON.stringify(description || "")}`,
39
+ "---",
40
+ ].join("\n");
41
+ return `${fm}\n\n${body}\n`;
42
+ }
43
+
30
44
  // Contenido del .env según el backend de PM elegido (tokens vacíos, a completar).
31
45
  export function envFor(pm) {
32
46
  const head = "# Config de dai — completá lo que falte. NUNCA commitees tokens (.env está gitignored).\n";
@@ -39,11 +53,68 @@ export function envFor(pm) {
39
53
  return head + "DAI_PM=md\nDAI_MD_US_DIR=.dai/us\n";
40
54
  }
41
55
 
56
+ // ── Helpers aditivos para `dai init` — no destruir la config de un repo vivo ──
57
+
58
+ // Une aditivamente variables de entorno: agrega solo las claves KEY= que NO estén
59
+ // ya definidas en `existing`. Idempotente; no toca lo demás. Ignora comentarios.
60
+ export function mergeEnv(existing, block) {
61
+ const keyOf = (line) => {
62
+ const m = line.match(/^\s*(?:export\s+)?([\w.-]+)\s*=/);
63
+ return m ? m[1] : null;
64
+ };
65
+ const have = new Set(existing.split(/\r?\n/).map(keyOf).filter(Boolean));
66
+ const toAdd = block.split(/\r?\n/).filter((line) => { const k = keyOf(line); return k && !have.has(k); });
67
+ if (toAdd.length === 0) return existing;
68
+ const sep = existing && !existing.endsWith("\n") ? "\n" : "";
69
+ return existing + sep + "\n# ── dai ──\n" + toAdd.join("\n") + "\n";
70
+ }
71
+
72
+ // Inserta o actualiza un bloque delimitado (marcadores HTML) en una constitución,
73
+ // sin pisar el resto del archivo. Idempotente: re-correr actualiza el bloque, no duplica.
74
+ export function upsertBlock(existing, block, marker = "dai") {
75
+ const start = `<!-- ${marker}:start -->`, end = `<!-- ${marker}:end -->`;
76
+ const wrapped = `${start}\n${block.trim()}\n${end}`;
77
+ const i = existing.indexOf(start), j = existing.indexOf(end);
78
+ if (i !== -1 && j > i) {
79
+ return existing.slice(0, i) + wrapped + existing.slice(j + end.length);
80
+ }
81
+ const base = existing.trimEnd();
82
+ return base ? `${base}\n\n${wrapped}\n` : `${wrapped}\n`;
83
+ }
84
+
85
+ // Ajusta el texto de un .gitignore para que los artefactos de dai (según --for)
86
+ // queden versionados, dejando fuera SOLO lo personal (settings.local.json). Quita
87
+ // los ignores "broad" (.claude, CLAUDE.md, .cursor) que los esconderían. Aditivo e
88
+ // idempotente. Devuelve { text, changed }.
89
+ export function reconcileGitignore(text, want) {
90
+ const norm = (s) => { let t = s.trim(); while (t.startsWith("/")) { t = t.slice(1); } while (t.endsWith("/")) { t = t.slice(0, -1); } return t; };
91
+ const broad = new Set();
92
+ const ensure = [".env"];
93
+ if (want.claude) { broad.add("CLAUDE.md"); broad.add(".claude"); ensure.push(".claude/settings.local.json"); }
94
+ if (want.cursor) { broad.add(".cursor"); }
95
+ let changed = false;
96
+ let lines = text.split(/\r?\n/).filter((line) => {
97
+ if (line.trim().startsWith("#")) return true;
98
+ if (broad.has(norm(line))) { changed = true; return false; }
99
+ return true;
100
+ });
101
+ const have = new Set(lines.map((l) => l.trim()));
102
+ const add = ensure.filter((e) => !have.has(e));
103
+ if (add.length) {
104
+ changed = true;
105
+ if (lines.length && lines[lines.length - 1].trim() !== "") lines.push("");
106
+ lines.push("# dai — solo lo personal fuera del control de versiones", ...add);
107
+ }
108
+ return { text: lines.join("\n"), changed };
109
+ }
110
+
42
111
  // La constitución del repo: se inyecta como CLAUDE.md (Claude) o
43
112
  // copilot-instructions.md (Copilot). Mismo núcleo, distinto encabezado.
44
113
  export function constitution(kind) {
45
114
  const head = kind === "copilot"
46
- ? "# Instrucciones de Copilot para este repo\n\nEste repo sigue la metodología **dai**. Aplicá estas reglas en todo lo que generes.\n\n> **Superficie:** los prompts de dai (`.github/prompts/`) se invocan solo en VS Code /\n> JetBrains, o como custom agents en el Copilot CLI — no en la app standalone ni en\n> github.com. El CLI `dai` corre en cualquier terminal."
115
+ ? "# Instrucciones de Copilot para este repo\n\nEste repo sigue la metodología **dai**. Aplica estas reglas en todo lo que generes.\n\n> **Superficie:** los prompts de dai (`.github/prompts/`) se invocan solo en VS Code /\n> JetBrains, o como custom agents en el Copilot CLI — no en la app standalone ni en\n> github.com. El CLI `dai` corre en cualquier terminal."
116
+ : kind === "cursor"
117
+ ? "# Constitución del proyecto (dai)\n\nEste repo sigue la metodología **dai**. Estas reglas gobiernan todo el trabajo.\n\n> **Superficie:** las skills de dai (`.cursor/skills/`) se invocan con `/nombre-skill`\n> o cuando el agente las detecta por descripción. El CLI `dai` corre en cualquier terminal."
47
118
  : "# Constitución del proyecto (dai)\n\nEste repo sigue la metodología **dai**. Estas reglas gobiernan todo el trabajo.";
48
119
  return `${head}
49
120
 
@@ -59,8 +130,19 @@ export function constitution(kind) {
59
130
  - **No vibe coding:** toda implementación arranca de una US con criterios testeables.
60
131
  - **TDD:** test primero, por la interfaz pública; sobrevive a un refactor.
61
132
  - **El link se autora una vez** (\`implements.yaml\`); la cobertura se **deriva** (nunca a mano).
133
+ - **Verifica el comportamiento, no solo que compile:** que pase el chequeo estático o el build no prueba que funcione; ejercita el flujo real antes de darlo por hecho.
134
+ - **La IA confirma antes de construir:** el asistente declara que entendió esta constitución y la va a obedecer antes de generar código.
62
135
  - **Secretos:** en \`.env\` (nunca commiteados). git por **SSH**, APIs por **token scopeado**.
63
- - Separá el QUÉ (funcional) del CÓMO (técnico); no mezcles.
136
+ - **Docs vivas:** una constitución o arquitectura desactualizada es un defecto, no documentación.
137
+ - Separa el QUÉ (funcional) del CÓMO (técnico); no mezcles.
138
+
139
+ ## Buenas prácticas (agnósticas)
140
+
141
+ - **Confía en el borde, no en el input:** valida y autoriza en el límite de confianza; no aceptes identidad, permisos ni datos externos sin verificar.
142
+ - **"No existe" > "no autorizado":** si un recurso no pertenece a quien pregunta, responde "no encontrado" en vez de revelar que existe.
143
+ - **Aísla la E/S externa:** toda llamada a un servicio externo vive detrás de un borde dedicado; la capa de presentación no llama afuera directo.
144
+ - **Estático en rojo = no está hecho:** los errores de tipos/lint son bloqueantes, no deuda diferible.
145
+ - **Lo que cambia junto, vive junto:** coloca junto la lógica, la interfaz y los tests de una misma unidad.
64
146
 
65
147
  ## Herramientas
66
148
 
@@ -72,3 +154,14 @@ export function constitution(kind) {
72
154
  Detalle completo de la metodología: ${REPO_URL}
73
155
  `;
74
156
  }
157
+
158
+ // Regla de Cursor always-on con el contenido de la constitución (sin título H1).
159
+ export function constitutionCursorRule() {
160
+ const content = constitution("cursor").replace(/^# .+\n\n/, "");
161
+ return `---
162
+ description: Metodología dai — constitución del proyecto (valores, reglas, herramientas)
163
+ alwaysApply: true
164
+ ---
165
+
166
+ ${content}`;
167
+ }
package/cli/lib/env.mjs CHANGED
@@ -16,6 +16,12 @@ export function loadEnv(path = ".env", env = process.env) {
16
16
  val = val.trim();
17
17
  if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
18
18
  val = val.slice(1, -1);
19
+ } else {
20
+ // Valor sin comillas: recortar comentario inline (espacio + #). Evita que un
21
+ // `TOKEN=xxx # nota` meta la nota — y caracteres no-ASCII como ← — en el valor,
22
+ // que después rompen headers HTTP (ByteString). Para conservar un # literal, usar comillas.
23
+ const c = val.search(/\s#/);
24
+ if (c !== -1) val = val.slice(0, c).trimEnd();
19
25
  }
20
26
  if (!(key in env)) env[key] = val;
21
27
  }
@@ -0,0 +1,73 @@
1
+ # ADR-0008 — Estrategia de internacionalización (i18n)
2
+
3
+ - **Estado:** propuesto
4
+ - **Fecha:** 2026-07-06
5
+ - **Decide:** lead / arquitecto de la metodología
6
+
7
+ ## Contexto
8
+
9
+ `dai` nació y se escribió **en español**. Para su difusión —el README es lo primero que
10
+ se ve en npm y GitHub, y el público de open source es mayoritariamente anglófono— hace
11
+ falta una versión en **inglés**. Pero traducir un proyecto tiene trampas: superficies muy
12
+ distintas (docs, landing HTML, mensajes del CLI, skills que son prompts), y el riesgo de
13
+ que las traducciones **se desincronicen** del original con cada cambio.
14
+
15
+ Dos cosas juegan a favor: (1) el tono ya es **español neutro internacional** (se sacó el
16
+ voseo y los modismos a propósito), que traduce limpio; y (2) `dai` puede **dogfood-earse**
17
+ —usar un agente para traducir de forma consistente— si se le fija la terminología.
18
+
19
+ La decisión es cómo estructurar el i18n para que sea **mantenible** y no genere drift.
20
+
21
+ ## Decisión
22
+
23
+ Adoptamos un modelo de **fuente única + traducciones derivadas**, por superficie:
24
+
25
+ 1. **Fuente de verdad: el español.** Se escribió primero y es el canónico. Cada archivo
26
+ traducido lleva una nota — *"Translation of the Spanish source; if they diverge, the
27
+ Spanish wins."* — igual que METODOLOGIA marca a los HTML como vistas derivadas.
28
+
29
+ 2. **Estructura de carpetas.** `docs/es/` (fuente) y `docs/en/` (traducción). En la raíz,
30
+ `README.es.md` + `README.en.md`, y `README.md` con un **selector de idioma** arriba
31
+ (`🇪🇸 Español · 🇬🇧 English`). El landing: una versión por idioma o un toggle en la página.
32
+
33
+ 3. **CLI: locale por variable, catálogo cero-dependencias.** El idioma sale de
34
+ `DAI_LANG` (default `es`; si no, se puede inferir de `LANG`). Los strings viven en un
35
+ catálogo (`cli/lib/i18n.mjs`, objeto `{ es, en }`) accedido por una función `t(key,…)`.
36
+ Sin librería de i18n — fiel al ethos de cero dependencias (ADR-0006).
37
+
38
+ 4. **Skills: se generan en el idioma elegido.** `dai init --lang en|es` copia la variante
39
+ del `SKILL.md`. La IA ya es multilingüe; solo cambia el idioma de las instrucciones.
40
+
41
+ 5. **Glosario de términos ES→EN fijo.** Para que la terminología no varíe entre archivos:
42
+ QUÉ→WHAT, CÓMO→HOW, estampar→stamp, trazabilidad→traceability, atrasado→stale, etc. Es
43
+ el insumo que guía la traducción (humana o asistida).
44
+
45
+ 6. **Implementación por fases** (no todo de una): (1) README + landing en inglés · (2) docs
46
+ core (MANIFIESTO, METODOLOGIA, glosario, EJEMPLO, guías) · (3) CLI i18n + skills · (4) el
47
+ resto (detalle/, ADRs, templates). Se prioriza por alcance, no por completitud.
48
+
49
+ ## Consecuencias
50
+
51
+ - ✅ Alcance internacional con el README/landing en inglés (fase 1) sin reescribir todo.
52
+ - ✅ Regla anti-drift clara: una sola fuente de verdad; las traducciones nunca "ganan".
53
+ - ✅ El CLI localiza sin sumar dependencias; `DAI_LANG` es opt-in (default español).
54
+ - ✅ La terminología queda estable entre superficies gracias al glosario ES→EN.
55
+ - ⚠️ Doble mantenimiento: cada cambio en un `.md` fuente obliga a re-traducir su par. Se
56
+ mitiga traduciendo con un agente + el glosario, y aceptando *lag* temporal en `en/`.
57
+ - ⚠️ El CLI necesita un refactor de strings (de literales a `t(key)`) — es mecánico pero toca
58
+ muchos puntos; se hace de una sola vez.
59
+ - ⚠️ Hay que decidir, al implementar, si el `README.md` raíz redirige (selector) o **es** una
60
+ de las lenguas (npm muestra el `README.md` — probablemente convenga inglés ahí).
61
+
62
+ ## Alternativas consideradas
63
+
64
+ - **Un framework de docs con i18n (Docusaurus / VitePress / mkdocs)** — descartado por ahora:
65
+ aporta i18n y versionado, pero cambia todo el setup de docs (hoy markdown plano + un HTML
66
+ autocontenido) y suma tooling/dependencias pesadas para un proyecto en `0.x`. Reconsiderable
67
+ si la doc crece mucho.
68
+ - **Inglés como fuente de verdad** — descartado: el contenido se authoró en español; invertir
69
+ la fuente ahora duplicaría el riesgo de error justo en el material canónico (manifiesto,
70
+ metodología). El español queda como fuente hasta que haya razón fuerte para migrar.
71
+ - **Traducción automática sin glosario ni fuente fija** — descartado: garantiza drift
72
+ terminológico (el mismo término traducido distinto en cada archivo) y pérdida de la
73
+ precisión del método. La traducción se ancla a un glosario y a una fuente única.
@@ -0,0 +1,63 @@
1
+ # ADR-0009 — Adaptador nativo para Cursor
2
+
3
+ - **Estado:** propuesto
4
+ - **Fecha:** 2026-07-07
5
+ - **Decide:** lead / arquitecto de la metodología
6
+
7
+ ## Contexto
8
+
9
+ ADR-0002 consolidó la estrategia agnóstica por asistente con tres capas: contenido
10
+ portable, CLI determinista y adaptadores delgados/generados por asistente. La
11
+ implementación actual cubre Claude y Copilot, pero no Cursor de forma nativa.
12
+
13
+ En la práctica, equipos que usan Cursor quedan con una experiencia incompleta: no
14
+ hay scaffolding en `.cursor/skills/`, no existe constitución always-on en rules, y
15
+ `dai doctor` / `dai install` no diagnostican ni instalan superficies de Cursor.
16
+
17
+ Cursor soporta skills (`.cursor/skills/<name>/SKILL.md`) y rules
18
+ (`.cursor/rules/*.mdc`) de forma nativa, y OpenSpec soporta `cursor` en `--tools`.
19
+ Eso permite agregar soporte sin romper el contrato existente ni duplicar lógica.
20
+
21
+ ## Decisión
22
+
23
+ Incorporamos Cursor como tercer adaptador generado en la capa 3:
24
+
25
+ 1. `dai init --for` acepta `cursor` y `all`, además de `claude`, `copilot`, `both`.
26
+ 2. El default de `dai init` pasa a `all` (Claude + Copilot + Cursor).
27
+ 3. `both` mantiene su significado histórico (Claude + Copilot), para compatibilidad
28
+ semántica.
29
+ 4. Las skills de Cursor se generan en `.cursor/skills/` desde la misma fuente
30
+ (`skills/*/SKILL.md`) con `skillToCursor()`.
31
+ 5. El transformador de Cursor conserva `name`, `description` y cuerpo, y no agrega
32
+ `disable-model-invocation` (se permite auto-invocación por descripción).
33
+ 6. La constitución de Cursor se genera como rule `.mdc` en
34
+ `.cursor/rules/dai-constitution.mdc` con `alwaysApply: true`.
35
+ 7. `dai doctor` reporta skills y constitución en rutas locales/globales de Cursor.
36
+ 8. `dai install` soporta Cursor vía `--for claude|cursor|all` (default `all`).
37
+ 9. OpenSpec mapea `--for all` a `claude,github-copilot,cursor`.
38
+
39
+ ## Consecuencias
40
+
41
+ - ✅ La metodología mantiene su principio agnóstico también para Cursor, sin bifurcar
42
+ contenido de skills.
43
+ - ✅ Equipos mixtos pueden coexistir en un repo con Claude, Copilot y Cursor sin
44
+ pisarse (carpetas separadas).
45
+ - ✅ El runtime mecánico sigue centralizado en CLI (`dai`), sin mover lógica
46
+ determinista a prompts.
47
+ - ✅ Cursor recibe constitución always-on alineada a Claude/Copilot.
48
+ - ⚠️ Cambia el comportamiento por defecto de `dai init` (de `both` a `all`); requiere
49
+ documentación clara para evitar sorpresa.
50
+ - ⚠️ `dai install` ahora maneja más de un target de asistente y aumenta la superficie
51
+ de mantenimiento.
52
+
53
+ ## Alternativas consideradas
54
+
55
+ - **Depender de `.claude/skills/` por compatibilidad de Cursor** — descartado: no da
56
+ soporte profesional de rules/doctor/install y contradice la idea de adaptador
57
+ nativo generado.
58
+ - **Redefinir `both` para incluir Cursor** — descartado: rompe semántica histórica del
59
+ valor; se prefiere agregar `all`.
60
+ - **Forzar `disable-model-invocation: true` en Cursor** — descartado para este repo:
61
+ se busca paridad de auto-invocación con Claude.
62
+ - **Constitución de Cursor en `AGENTS.md`** — descartado: la superficie nativa always-on
63
+ de Cursor son `.cursor/rules/*.mdc`.
@@ -13,6 +13,8 @@ decisión cambia, se escribe un ADR nuevo que supersede al viejo. Molde en
13
13
  | [0005](0005-superficie-comandos-y-stamp.md) | Superficie de comandos (`ls`/`check`/`stamp`) y contenido del stamp (branch + commit-ancla) | aceptado |
14
14
  | [0006](0006-distribucion-y-licencia.md) | Distribución (npm + fallback) y licencia (GPLv3) | aceptado |
15
15
  | [0007](0007-modelo-de-autenticacion.md) | Modelo de auth: SSH para git, tokens scopeados para forge/tracker, sin contraseñas | aceptado |
16
+ | [0008](0008-estrategia-de-i18n.md) | Estrategia de i18n: fuente única (español) + traducciones derivadas, `DAI_LANG` en el CLI, por fases | propuesto |
17
+ | [0009](0009-adaptador-cursor.md) | Adaptador nativo para Cursor (skills + rules) con `dai init`/`install`/`doctor` | propuesto |
16
18
 
17
19
  > Estas son las decisiones que cierran las "Decisiones abiertas" de
18
20
  > [`METODOLOGIA.md §7`](../METODOLOGIA.md) y las enmiendas al
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dforce2055/dai",
3
- "version": "0.1.1",
3
+ "version": "0.3.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/",
@@ -30,7 +30,6 @@
30
30
  "templates",
31
31
  "governance",
32
32
  "skills",
33
- "index.html",
34
33
  "manifest.yaml",
35
34
  "install.sh",
36
35
  ".env.example",
package/index.html DELETED
@@ -1,479 +0,0 @@
1
- <!doctype html>
2
- <html lang="es">
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width, initial-scale=1" />
6
- <title>dai — Desarrollo Asistido por IA con trazabilidad</title>
7
- <meta name="description" content="Un método y un CLI para que la IA programe con trazabilidad y sin vibe coding. Separa el QUÉ del CÓMO, linkéalos una vez, y que la máquina avise sola cuando se desincronizan. Se adapta a cualquier tamaño de equipo." />
8
- <!-- Favicon SVG inline (sin CDN, nítido en cualquier resolución) -->
9
- <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Cdefs%3E%3ClinearGradient id='g' x1='0' y1='0' x2='1' y2='1'%3E%3Cstop offset='0' stop-color='%236366f1'/%3E%3Cstop offset='1' stop-color='%230ea5e9'/%3E%3C/linearGradient%3E%3C/defs%3E%3Crect width='64' height='64' rx='14' fill='url(%23g)'/%3E%3Ctext x='32' y='48' font-family='ui-sans-serif,system-ui,Arial' font-size='44' font-weight='800' fill='white' text-anchor='middle'%3Ed%3C/text%3E%3C/svg%3E" />
10
- <meta name="theme-color" content="#6366f1" />
11
- <style>
12
- /* Autocontenido: fuentes del sistema + diagramas en CSS. Sin CDN → anda offline. */
13
- :root{
14
- --bg:#0b1120; --card:#131c2e; --ink:#eef2f8; --mut:#94a3b8; --line:#263247;
15
- --brand:#6366f1; --brand-dark:#4f46e5;
16
- --func:#f59e0b; --func-soft:#3a2f12; --func-ink:#fbbf24; /* el QUÉ */
17
- --tech:#0ea5e9; --tech-soft:#10273b; --tech-ink:#7dd3fc; /* el CÓMO */
18
- --ci:#10b981; --ci-soft:#0f2c22; --ci-ink:#6ee7b7; /* la máquina */
19
- --danger:#ef4444;
20
- --mono:ui-monospace,"SF Mono",Menlo,Consolas,monospace;
21
- --sans:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
22
- }
23
- :root[data-theme="light"]{
24
- --bg:#f8fafc; --card:#ffffff; --ink:#0f172a; --mut:#475569; --line:#e2e8f0;
25
- --func-soft:#fef3c7; --func-ink:#92400e; --tech-soft:#e0f2fe; --tech-ink:#075985;
26
- --ci-soft:#d1fae5; --ci-ink:#065f46;
27
- }
28
- @media (prefers-color-scheme: light){
29
- :root:not([data-theme="dark"]){
30
- --bg:#f8fafc; --card:#ffffff; --ink:#0f172a; --mut:#475569; --line:#e2e8f0;
31
- --func-soft:#fef3c7; --func-ink:#92400e; --tech-soft:#e0f2fe; --tech-ink:#075985;
32
- --ci-soft:#d1fae5; --ci-ink:#065f46;
33
- }
34
- }
35
- *{box-sizing:border-box}
36
- html{scroll-behavior:smooth}
37
- body{margin:0;background:var(--bg);color:var(--ink);font-family:var(--sans);
38
- line-height:1.6;-webkit-font-smoothing:antialiased}
39
- .wrap{max-width:960px;margin:0 auto;padding:0 20px}
40
- a{color:var(--brand);text-decoration:none}
41
- a:hover{text-decoration:underline}
42
- code,kbd{font-family:var(--mono);font-size:.9em}
43
- h1,h2,h3{line-height:1.2;letter-spacing:-.02em}
44
- section{padding:64px 0;border-top:1px solid var(--line)}
45
- .eyebrow{font-family:var(--mono);font-size:.8rem;letter-spacing:.08em;
46
- text-transform:uppercase;color:var(--mut);margin:0 0 8px}
47
- h2{font-size:clamp(1.5rem,4vw,2rem);margin:0 0 8px}
48
- .lede{color:var(--mut);font-size:1.05rem;max-width:62ch;margin:0 0 28px}
49
-
50
- /* ---------- nav ---------- */
51
- nav{position:sticky;top:0;z-index:10;backdrop-filter:blur(10px);
52
- background:color-mix(in srgb,var(--bg) 82%,transparent);border-bottom:1px solid var(--line)}
53
- nav .wrap{display:flex;align-items:center;gap:16px;height:56px}
54
- nav .logo{font-weight:800;letter-spacing:-.03em}
55
- nav .logo b{color:var(--brand)}
56
- nav .sp{flex:1}
57
- nav a.nl{color:var(--mut);font-size:.9rem}
58
- nav a.nl:hover{color:var(--ink);text-decoration:none}
59
- .toggle{cursor:pointer;border:1px solid var(--line);background:var(--card);
60
- color:var(--ink);border-radius:8px;padding:5px 9px;font-size:.85rem}
61
- @media(max-width:640px){nav a.nl{display:none}}
62
-
63
- /* ---------- hero ---------- */
64
- .hero{padding:80px 0 64px;text-align:center}
65
- .hero h1{font-size:clamp(2.2rem,7vw,3.6rem);margin:0 0 16px;font-weight:900}
66
- .hero h1 .g{background:linear-gradient(120deg,var(--brand),var(--tech));
67
- -webkit-background-clip:text;background-clip:text;color:transparent}
68
- .hero p{color:var(--mut);font-size:1.2rem;max-width:60ch;margin:0 auto 28px}
69
- .cta{display:flex;gap:12px;justify-content:center;flex-wrap:wrap}
70
- .btn{display:inline-block;padding:11px 20px;border-radius:10px;font-weight:600;font-size:.95rem}
71
- .btn.p{background:var(--brand);color:#fff}
72
- .btn.p:hover{background:var(--brand-dark);text-decoration:none}
73
- .btn.s{border:1px solid var(--line);color:var(--ink)}
74
- .btn.s:hover{text-decoration:none;border-color:var(--brand)}
75
- .install{margin:26px auto 0;max-width:520px;background:var(--card);border:1px solid var(--line);
76
- border-radius:12px;padding:14px 16px;font-family:var(--mono);font-size:.9rem;text-align:left;
77
- color:var(--ink);overflow-x:auto}
78
- .install .c{color:var(--ci-ink)}
79
- .install .m{color:var(--mut)}
80
-
81
- /* ---------- pills / cards ---------- */
82
- .grid{display:grid;gap:16px}
83
- .g3{grid-template-columns:repeat(3,1fr)}
84
- .g2{grid-template-columns:repeat(2,1fr)}
85
- @media(max-width:720px){.g3,.g2{grid-template-columns:1fr}}
86
- .card{background:var(--card);border:1px solid var(--line);border-radius:14px;padding:20px;
87
- transition:transform .25s ease,border-color .25s ease,box-shadow .25s ease}
88
- .card:hover{transform:translateY(-4px);border-color:color-mix(in srgb,var(--brand) 55%,var(--line));
89
- box-shadow:0 12px 30px -12px color-mix(in srgb,var(--brand) 45%,transparent)}
90
- .card h3{margin:0 0 6px;font-size:1.05rem}
91
- .card p{margin:0;color:var(--mut);font-size:.95rem}
92
-
93
- /* ---------- animaciones (reveal al scroll + hover) ---------- */
94
- .reveal{transition:opacity .7s ease,transform .7s cubic-bezier(.2,.7,.2,1)}
95
- html.anim .reveal{opacity:0;transform:translateY(20px)}
96
- html.anim .reveal.in{opacity:1;transform:none}
97
- .lvl,.step .body,.side{transition:transform .25s ease}
98
- .lvl:hover{transform:translateY(-4px)}
99
- .install{transition:border-color .25s ease}
100
- .install:hover{border-color:color-mix(in srgb,var(--ci) 50%,var(--line))}
101
- @media (prefers-reduced-motion: reduce){
102
- html.anim .reveal{opacity:1 !important;transform:none !important}
103
- .card:hover,.lvl:hover{transform:none}
104
- }
105
- .ic{width:34px;height:34px;border-radius:9px;display:grid;place-items:center;
106
- font-size:1.1rem;margin-bottom:12px}
107
- .ic{transition:transform .25s ease}
108
- .ic.d{background:linear-gradient(135deg,color-mix(in srgb,var(--brand) 28%,transparent),color-mix(in srgb,var(--tech) 28%,transparent));
109
- box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--brand) 24%,transparent)}
110
- .ic.b{background:linear-gradient(135deg,color-mix(in srgb,var(--brand) 26%,transparent),color-mix(in srgb,var(--tech) 26%,transparent))}
111
- .card:hover .ic{transform:scale(1.08) rotate(-3deg)}
112
-
113
- .tag{display:inline-block;font-family:var(--mono);font-size:.72rem;font-weight:600;
114
- padding:2px 8px;border-radius:999px;letter-spacing:.02em}
115
- .tag.q{background:var(--func-soft);color:var(--func-ink)}
116
- .tag.c{background:var(--tech-soft);color:var(--tech-ink)}
117
- .tag.m{background:var(--ci-soft);color:var(--ci-ink)}
118
-
119
- /* ---------- QUÉ vs CÓMO ---------- */
120
- .split{display:grid;grid-template-columns:1fr auto 1fr;gap:0;align-items:stretch;
121
- background:var(--card);border:1px solid var(--line);border-radius:16px;overflow:hidden}
122
- @media(max-width:720px){.split{grid-template-columns:1fr}}
123
- .side{padding:24px}
124
- .side.q{border-right:1px solid var(--line)}
125
- @media(max-width:720px){.side.q{border-right:0;border-bottom:1px solid var(--line)}}
126
- .side h4{margin:0 0 4px;font-size:1.1rem}
127
- .side .who{color:var(--mut);font-size:.85rem;margin:0 0 14px}
128
- .side ul{margin:0;padding-left:18px;color:var(--mut);font-size:.92rem}
129
- .link{display:grid;place-items:center;padding:0 8px;background:
130
- color-mix(in srgb,var(--brand) 10%,transparent)}
131
- .link .b{font-family:var(--mono);font-size:.72rem;color:var(--brand);
132
- writing-mode:vertical-rl;transform:rotate(180deg);letter-spacing:.1em;font-weight:700}
133
- @media(max-width:720px){.link{padding:10px}.link .b{writing-mode:horizontal-tb;transform:none}}
134
-
135
- /* ---------- steps / journey ---------- */
136
- .steps{position:relative;margin-top:8px}
137
- .step{display:grid;grid-template-columns:44px 1fr;gap:18px;padding:14px 0;position:relative}
138
- .step:not(:last-child)::before{content:"";position:absolute;left:21px;top:44px;bottom:-14px;
139
- width:2px;background:var(--line)}
140
- .num{width:44px;height:44px;border-radius:12px;display:grid;place-items:center;
141
- font-weight:800;font-family:var(--mono);background:var(--card);border:1px solid var(--line);z-index:1}
142
- .step.q .num{border-color:var(--func);color:var(--func-ink)}
143
- .step.c .num{border-color:var(--tech);color:var(--tech-ink)}
144
- .step.m .num{border-color:var(--ci);color:var(--ci-ink)}
145
- .step .body h3{margin:0 0 3px;font-size:1.08rem}
146
- .step .body p{margin:0 0 8px;color:var(--mut);font-size:.95rem}
147
- .step .tool{font-family:var(--mono);font-size:.82rem;background:var(--bg);
148
- border:1px solid var(--line);border-radius:8px;padding:3px 9px;display:inline-block;color:var(--ink)}
149
-
150
- /* ---------- traceability demo ---------- */
151
- .demo{background:var(--card);border:1px solid var(--line);border-radius:16px;padding:8px}
152
- .term{font-family:var(--mono);font-size:.86rem;background:#0a0f1e;border-radius:12px;
153
- padding:18px 18px;color:#cbd5e1;overflow-x:auto}
154
- :root[data-theme="light"] .term{background:#0f172a;color:#e2e8f0}
155
- @media(prefers-color-scheme:light){:root:not([data-theme="dark"]) .term{background:#0f172a;color:#e2e8f0}}
156
- .term .p{color:#64748b}
157
- .term .ok{color:#6ee7b7}
158
- .term .warn{color:#fca5a5}
159
- .term .cmd{color:#e2e8f0}
160
- .term .dim{color:#64748b}
161
- .term b{color:#fff;font-weight:600}
162
-
163
- /* ---------- dial ---------- */
164
- .dial{display:grid;grid-template-columns:repeat(3,1fr);gap:14px}
165
- @media(max-width:720px){.dial{grid-template-columns:1fr}}
166
- .lvl{background:var(--card);border:1px solid var(--line);border-radius:14px;padding:18px}
167
- .lvl .n{font-family:var(--mono);font-size:.78rem;color:var(--brand);font-weight:700}
168
- .lvl h4{margin:6px 0 4px;font-size:1.05rem}
169
- .lvl p{margin:0;color:var(--mut);font-size:.9rem}
170
-
171
- /* ---------- start ---------- */
172
- ol.start{counter-reset:s;list-style:none;padding:0;margin:0}
173
- ol.start li{position:relative;padding:0 0 18px 40px}
174
- ol.start li::before{counter-increment:s;content:counter(s);position:absolute;left:0;top:0;
175
- width:26px;height:26px;border-radius:8px;background:var(--brand);color:#fff;
176
- display:grid;place-items:center;font-weight:700;font-size:.85rem}
177
- ol.start code{background:var(--card);border:1px solid var(--line);border-radius:7px;
178
- padding:2px 7px;display:inline-block}
179
- ol.start .d{color:var(--mut);font-size:.9rem}
180
-
181
- footer{border-top:1px solid var(--line);padding:40px 0 60px;color:var(--mut);font-size:.9rem}
182
- footer .wrap{display:flex;gap:20px;flex-wrap:wrap;align-items:center}
183
- footer .sp{flex:1}
184
- </style>
185
- </head>
186
- <body>
187
-
188
- <nav>
189
- <div class="wrap">
190
- <span class="logo"><b>dai</b> · Desarrollo Asistido por IA</span>
191
- <span class="sp"></span>
192
- <a class="nl" href="#problema">Problema</a>
193
- <a class="nl" href="#flujo">El flujo</a>
194
- <a class="nl" href="#traza">Trazabilidad</a>
195
- <a class="nl" href="#empezar">Empezar</a>
196
- <button class="toggle" id="tg" aria-label="Cambiar tema">◐</button>
197
- </div>
198
- </nav>
199
-
200
- <!-- ══════════ HERO ══════════ -->
201
- <header class="hero wrap">
202
- <h1>La IA programa. <span class="g">@dai te da la trazabilidad.</span></h1>
203
- <p>Un método —y un CLI— para desarrollo asistido por IA <b>sin vibe coding</b> y con
204
- trazabilidad real: separa el <span class="tag q">QUÉ</span> del
205
- <span class="tag c">CÓMO</span>, linkéalos una sola vez, y deja que la máquina
206
- avise sola cuando se desincronizan. De un dev solo a una organización entera —
207
- el mismo método.</p>
208
- <div class="cta">
209
- <a class="btn p" href="#empezar">Empezar ahora</a>
210
- <a class="btn s" href="https://github.com/dforce2055/dai">Ver en GitHub</a>
211
- </div>
212
- <div class="install">
213
- <span class="m"># instala el CLI (Node ≥18, cero dependencias)</span><br>
214
- <span class="c">npm i -g @dforce2055/dai</span><br>
215
- <span class="c">dai init</span> <span class="m"># configura tu repo: Claude/Copilot + OpenSpec + el link</span>
216
- </div>
217
- </header>
218
-
219
- <!-- ══════════ PROBLEMA ══════════ -->
220
- <section id="problema">
221
- <div class="wrap">
222
- <p class="eyebrow">El problema</p>
223
- <h2>La IA acelera el código. Y también acelera el caos.</h2>
224
- <p class="lede">Tres dolores que la IA no resuelve sola — al contrario, los amplifica
225
- si no hay método.</p>
226
- <div class="grid g3">
227
- <div class="card">
228
- <div class="ic d">🔌</div>
229
- <h3>Desincronización silenciosa</h3>
230
- <p>El requerimiento cambia, el código sigue con la versión vieja, y se descubre
231
- recién en la demo. Nadie se enteró a tiempo.</p>
232
- </div>
233
- <div class="card">
234
- <div class="ic d">🎲</div>
235
- <h3>Vibe coding</h3>
236
- <p>La IA construye sobre una idea vaga y sale cualquier cosa. Rápido, sí — pero
237
- sin garantía de que sea lo que se pedía.</p>
238
- </div>
239
- <div class="card">
240
- <div class="ic d">🔀</div>
241
- <h3>QUÉ y CÓMO mezclados</h3>
242
- <p>El funcional se mete en la implementación; el dev redefine el negocio. Se pierde
243
- quién es dueño de qué.</p>
244
- </div>
245
- </div>
246
- </div>
247
- </section>
248
-
249
- <!-- ══════════ LA IDEA ══════════ -->
250
- <section id="idea">
251
- <div class="wrap">
252
- <p class="eyebrow">La idea</p>
253
- <h2>Separa el QUÉ del CÓMO. Linkéalos una sola vez.</h2>
254
- <p class="lede">El requerimiento (el QUÉ) y la implementación (el CÓMO) son cosas
255
- distintas, con dueños distintos. dai las mantiene atadas — y la dirección inversa
256
- se <b>deriva</b>, nunca se escribe a mano.</p>
257
-
258
- <div class="split">
259
- <div class="side q">
260
- <span class="tag q">EL QUÉ · funcional</span>
261
- <h4 style="margin-top:12px">La User Story</h4>
262
- <p class="who">dueño: PO / analista · vive en Jira o ClickUp</p>
263
- <ul>
264
- <li>Criterios de aceptación testeables</li>
265
- <li>Lo produce una skill de IA por interrogación</li>
266
- <li>Identidad estable: el ticket del tracker</li>
267
- </ul>
268
- </div>
269
- <div class="link"><span class="b">implements →</span></div>
270
- <div class="side">
271
- <span class="tag c">EL CÓMO · técnico</span>
272
- <h4 style="margin-top:12px">El código + su spec</h4>
273
- <p class="who">dueño: dev / ingeniero · vive en el repo</p>
274
- <ul>
275
- <li>Declara <code>implements: US@versión</code> — una vez</li>
276
- <li>Diseño y tareas generados con OpenSpec</li>
277
- <li>Se construye con tests primero (TDD)</li>
278
- </ul>
279
- </div>
280
- </div>
281
-
282
- <div class="grid g3" style="margin-top:16px">
283
- <div class="card"><span class="tag q">1</span><h3 style="margin-top:10px">Identidad estable</h3>
284
- <p>El QUÉ nace con un ID único (el ticket). Es lo único que lo hace linkeable.</p></div>
285
- <div class="card"><span class="tag c">2</span><h3 style="margin-top:10px">Un solo link, a mano</h3>
286
- <p>El código apunta al QUÉ. La cobertura inversa (quién implementó qué) la genera la máquina.</p></div>
287
- <div class="card"><span class="tag m">3</span><h3 style="margin-top:10px">@version que grita</h3>
288
- <p>Número legible + hash de criterios. Si el QUÉ cambia, los CÓMO atrasados se marcan solos.</p></div>
289
- </div>
290
- </div>
291
- </section>
292
-
293
- <!-- ══════════ EL FLUJO GUIADO ══════════ -->
294
- <section id="flujo">
295
- <div class="wrap">
296
- <p class="eyebrow">El flujo</p>
297
- <h2>Tu Scrum de siempre — con IA en cada paso</h2>
298
- <p class="lede">dai no reemplaza tu proceso: lo potencia. En cada etapa hay una skill
299
- de IA o un comando que hace el trabajo pesado. <span class="tag q">QUÉ</span>
300
- <span class="tag c">CÓMO</span> <span class="tag m">máquina</span></p>
301
-
302
- <div class="steps">
303
- <div class="step q"><div class="num">1</div><div class="body">
304
- <h3>Define el QUÉ</h3>
305
- <p>El analista describe la idea. Una skill de IA lo <b>interroga</b> hasta que la
306
- User Story es testeable, y la publica en el tracker. La IA no inventa: pregunta.</p>
307
- <span class="tool">/grill-intent → /grill-user-story → ClickUp / Jira</span>
308
- </div></div>
309
-
310
- <div class="step c"><div class="num">2</div><div class="body">
311
- <h3>Abre el CÓMO, atado al QUÉ</h3>
312
- <p>El dev arranca la implementación. dai trae la US del tracker, crea la branch y
313
- siembra el link — <b>correcto por construcción</b>, sin tipear el ID a mano.</p>
314
- <span class="tool">dai link-us &lt;ID&gt; → branch + implements.yaml</span>
315
- </div></div>
316
-
317
- <div class="step c"><div class="num">3</div><div class="body">
318
- <h3>Diseña con OpenSpec</h3>
319
- <p>OpenSpec toma la US y genera la propuesta, el diseño y las tareas — co-localizados
320
- con el link, en la misma branch. El QUÉ se convierte en un plan técnico.</p>
321
- <span class="tool">/opsx:propose → proposal · design · tasks</span>
322
- </div></div>
323
-
324
- <div class="step c"><div class="num">4</div><div class="body">
325
- <h3>Implementa con tests primero</h3>
326
- <p>TDD en rebanadas verticales: un test, el código mínimo, repetir. La IA escribe el
327
- test como spec ejecutable. Anti vibe-coding de verdad.</p>
328
- <span class="tool">/tdd · red → green → refactor</span>
329
- </div></div>
330
-
331
- <div class="step m"><div class="num">5</div><div class="body">
332
- <h3>Verifica la trazabilidad</h3>
333
- <p>¿Tu código sigue al día con la US? dai re-deriva el hash de la US viva y compara.
334
- Si el QUÉ cambió, te avisa <b>y te dice cómo resincronizar</b>.</p>
335
- <span class="tool">dai check → ✅ al día / ⚠️ atrasado</span>
336
- </div></div>
337
-
338
- <div class="step c"><div class="num">6</div><div class="body">
339
- <h3>Crea y revisa la PR</h3>
340
- <p>dai arma el texto de la PR desde el template, precargado con la US, el estado del
341
- check y los enlaces. Tu PR la revisas tú; la de un compañero, con la skill de review.</p>
342
- <span class="tool">dai pr · /dai-review (la PR de otro)</span>
343
- </div></div>
344
-
345
- <div class="step m"><div class="num">7</div><div class="body">
346
- <h3>Mergea y estampa la cobertura</h3>
347
- <p>Al mergear, dai escribe en el tracker <b>quién implementó qué</b>, contra qué
348
- versión, con branch y commit. La trazabilidad inversa — nadie la mantiene a mano.</p>
349
- <span class="tool">dai stamp → cobertura en el tracker</span>
350
- </div></div>
351
- </div>
352
- </div>
353
- </section>
354
-
355
- <!-- ══════════ TRAZABILIDAD (demo) ══════════ -->
356
- <section id="traza">
357
- <div class="wrap">
358
- <p class="eyebrow">La pieza que lo cambia todo</p>
359
- <h2>Cuando el QUÉ cambia, el desajuste se hace visible solo</h2>
360
- <p class="lede">El PO edita un criterio en el tracker. Tú no tocaste una línea de código.
361
- Y aun así, dai lo detecta — porque compara el hash de la US viva contra el que dejaste
362
- estampado.</p>
363
-
364
- <div class="demo">
365
- <div class="term">
366
- <span class="p">$</span> <span class="cmd">dai check</span>
367
- <span class="ok">✅ 86ajcjwzk al día (v1)</span>
368
-
369
- <span class="dim"># … el PO agrega un criterio de aceptación en el tracker …</span>
370
-
371
- <span class="p">$</span> <span class="cmd">dai check</span>
372
- <span class="warn">⚠️ 86ajcjwzk ATRASADO: implementaste 2124e94f, la US viva es 51abbfa0</span>
373
-
374
- <span class="dim">El QUÉ cambió desde que lo implementaste. Para resincronizar:</span>
375
- <b>dai link-us 86ajcjwzk --resync</b> <span class="dim"># re-estampa contra la US viva</span>
376
- </div>
377
- </div>
378
- <p class="lede" style="margin-top:22px">Nadie le avisó al dev. El link versionado lo
379
- avisó solo. Esa es la diferencia entre <i>esperar que alguien se dé cuenta</i> y
380
- <i>que el sistema te lo diga</i>.</p>
381
- </div>
382
- </section>
383
-
384
- <!-- ══════════ SE ADAPTA ══════════ -->
385
- <section id="escala">
386
- <div class="wrap">
387
- <p class="eyebrow">Un solo método</p>
388
- <h2>Se adapta a cualquier tamaño de equipo</h2>
389
- <p class="lede">No hay una versión para equipos chicos y otra para grandes. Hay
390
- <b>un protocolo invariante</b> y un dial de ceremonia que sube o baja según la escala.
391
- Cada capa se agrega cuando duele, no antes.</p>
392
- <div class="dial">
393
- <div class="lvl"><span class="n">NIVEL 1</span><h4>Un dev solo</h4>
394
- <p>OpenSpec y el CLI, todo local. La US puede vivir como un <code>.md</code>. Cero
395
- infraestructura. El link y el check funcionan igual.</p></div>
396
- <div class="lvl"><span class="n">NIVEL 2</span><h4>Equipo compacto</h4>
397
- <p>Aparece el tracker (ClickUp/Jira) para coordinar. El link referencia la US; el
398
- check y el stamp corren a mano o en un hook.</p></div>
399
- <div class="lvl"><span class="n">NIVEL 3</span><h4>Organización grande</h4>
400
- <p>Muchos repos, equipos separados, CI que estampa cobertura y matriz de ambientes.
401
- El mismo protocolo, con más plomería.</p></div>
402
- </div>
403
- <p class="lede" style="margin-top:20px">El dev que aprende el nivel chico ya sabe el
404
- grande. Nadie mantiene dos metodologías.</p>
405
- </div>
406
- </section>
407
-
408
- <!-- ══════════ EMPEZAR ══════════ -->
409
- <section id="empezar">
410
- <div class="wrap">
411
- <p class="eyebrow">Empezar</p>
412
- <h2>De cero a tu primera US trazada</h2>
413
- <p class="lede">Requiere Node ≥ 18. El CLI no tiene dependencias — anda incluso en redes
414
- corporativas cerradas.</p>
415
- <ol class="start">
416
- <li><code>npm i -g @dforce2055/dai</code>
417
- <div class="d">Instala el CLI. El comando queda <code>dai</code>.</div></li>
418
- <li><code>dai init</code>
419
- <div class="d">Scaffolder interactivo: eliges asistente (Claude/Copilot), tracker
420
- (md/ClickUp/Jira) y si sumas OpenSpec. Deja el repo listo.</div></li>
421
- <li><code>dai link-us &lt;ID&gt;</code>
422
- <div class="d">Trae la US del tracker, crea la branch y el link. Implementas con TDD.</div></li>
423
- <li><code>dai check</code> → <code>dai pr</code> → <code>dai stamp</code>
424
- <div class="d">Verificas que sigues al día, creas la PR precargada, y al mergear
425
- estampas la cobertura. El círculo cerrado.</div></li>
426
- </ol>
427
- <div class="cta" style="justify-content:flex-start;margin-top:8px">
428
- <a class="btn p" href="https://github.com/dforce2055/dai">Documentación completa</a>
429
- <a class="btn s" href="https://github.com/dforce2055/dai/blob/main/docs/EJEMPLO-END-TO-END.md">Ver un ejemplo real</a>
430
- </div>
431
- </div>
432
- </section>
433
-
434
- <footer>
435
- <div class="wrap">
436
- <span><b style="color:var(--brand)">dai</b> — Desarrollo Asistido por IA</span>
437
- <span class="sp"></span>
438
- <a href="https://github.com/dforce2055/dai">GitHub</a>
439
- <a href="https://github.com/dforce2055/dai/blob/main/docs/MANIFIESTO.md">Manifiesto</a>
440
- <a href="https://github.com/dforce2055/dai/blob/main/LICENSE">GPLv3</a>
441
- </div>
442
- </footer>
443
-
444
- <script>
445
- // JS mínimo: toggle de tema + animaciones de reveal. Nada externo.
446
- (function(){
447
- var r=document.documentElement, t=document.getElementById('tg');
448
- try{var s=localStorage.getItem('dai-theme'); if(s) r.setAttribute('data-theme',s);}catch(e){}
449
- t.addEventListener('click',function(){
450
- var cur=r.getAttribute('data-theme');
451
- var m=window.matchMedia('(prefers-color-scheme: dark)').matches?'dark':'light';
452
- var next=(cur||m)==='dark'?'light':'dark';
453
- r.setAttribute('data-theme',next);
454
- try{localStorage.setItem('dai-theme',next);}catch(e){}
455
- });
456
-
457
- // Reveal al hacer scroll (fade + slide up), con stagger por grupo.
458
- var reduce=window.matchMedia('(prefers-reduced-motion: reduce)').matches;
459
- var els=document.querySelectorAll(
460
- 'section h2, section .lede, .card, .step, .lvl, .split, .demo, .install, ol.start li, .cta');
461
- if(reduce || !('IntersectionObserver' in window)){ return; } // sin animar: todo visible
462
- r.classList.add('anim'); // recién acá ocultamos (fallback sin JS = visible)
463
- els.forEach(function(el){
464
- el.classList.add('reveal');
465
- // stagger: retraso según la posición del elemento entre sus hermanos
466
- var i=0,p=el.previousElementSibling;
467
- while(p){ i++; p=p.previousElementSibling; }
468
- el.style.transitionDelay=Math.min(i,6)*70+'ms';
469
- });
470
- var io=new IntersectionObserver(function(entries){
471
- entries.forEach(function(e){
472
- if(e.isIntersecting){ e.target.classList.add('in'); io.unobserve(e.target); }
473
- });
474
- },{threshold:.12, rootMargin:'0px 0px -8% 0px'});
475
- els.forEach(function(el){ io.observe(el); });
476
- })();
477
- </script>
478
- </body>
479
- </html>