@dforce2055/dai 0.3.0 → 0.3.1

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,26 @@
3
3
  Formato basado en [Keep a Changelog](https://keepachangelog.com/). Versionado semver
4
4
  (ver `VERSION`).
5
5
 
6
+ ## [0.3.1] — 2026-07-10
7
+
8
+ Pulido de la experiencia de `dai init` y `dai link-us`, y un ejemplo de US listo para probar.
9
+
10
+ ### Corregido
11
+ - **`dai init` · `.env.example`**: ahora refleja el `--pm` elegido — incluye todas las claves del
12
+ tracker (con el `..._TOKEN`), en vez del template genérico `md`. Antes, al elegir jira/clickup,
13
+ el `.env.example` quedaba con `DAI_PM=md` y sin el token.
14
+ - **`dai init` · `--for` con espacio**: mensaje claro cuando `--for claude, cursor` (con espacio) hace
15
+ que la shell parta la lista y un token de asistente caiga como `<repo>` ("no existe el directorio: cursor").
16
+ - **`dai link-us` · `dai ac-hash`**: mensaje accionable cuando la US no tiene sección
17
+ 'Criterios de aceptación' — sugiere agregarla o correr `/grill-user-story <ID>`.
18
+
19
+ ### Agregado
20
+ - **`templates/formato-us.md`**: ejemplo de US copy-paste al final (con criterios testeables) para
21
+ probar el flujo en 30 segundos, con o sin tracker (`dai ac-hash` / `dai link-us --us … --dry-run`).
22
+
23
+ ### Interno
24
+ - **105 tests** (+1 desde 0.3.0: `isAssistantToken`).
25
+
6
26
  ## [0.3.0] — 2026-07-08
7
27
 
8
28
  `dai init` ahora es **aditivo**: no pisa la configuración de un repo funcional. Más
@@ -102,6 +122,7 @@ ClickUp y Jira Cloud.
102
122
  - Tests de las rutas de red (jira/clickup/forge) con `fetch` mockeado. Sin links rotos;
103
123
  `files` de npm sin tests ni secretos.
104
124
 
125
+ [0.3.1]: https://github.com/dforce2055/dai/releases/tag/v0.3.1
105
126
  [0.3.0]: https://github.com/dforce2055/dai/releases/tag/v0.3.0
106
127
  [0.2.0]: https://github.com/dforce2055/dai/releases/tag/v0.2.0
107
128
  [0.1.1]: https://github.com/dforce2055/dai/releases/tag/v0.1.1
package/VERSION CHANGED
@@ -1 +1 @@
1
- 0.3.0
1
+ 0.3.1
package/cli/dai.mjs CHANGED
@@ -27,7 +27,7 @@ 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, parseAssistants } from "./lib/args.mjs";
30
+ import { parseFlags, parseAssistants, isAssistantToken } from "./lib/args.mjs";
31
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));
@@ -59,7 +59,7 @@ function trackerUrl(id) {
59
59
  function cmdAcHash(arg) {
60
60
  const md = arg ? readFileSync(arg, "utf8") : readFileSync(0, "utf8");
61
61
  const h = acHash(md);
62
- if (h == null) fail("la US no tiene bloque de 'Criterios de aceptación'.", 2);
62
+ if (h == null) fail("la US no tiene un bloque 'Criterios de aceptación' (criterios testeables bajo '## Criterios de aceptación').", 2);
63
63
  process.stdout.write(h + "\n");
64
64
  }
65
65
 
@@ -98,7 +98,7 @@ async function cmdLinkUs(key, opts) {
98
98
  // Fuente local: un .md con la US.
99
99
  const md = readFileSync(opts.us, "utf8");
100
100
  hash = acHash(md);
101
- if (hash == null) fail("la US no tiene 'Criterios de aceptación' → no se puede calcular ac_hash.", 2);
101
+ if (hash == null) fail(`la US en ${opts.us} no tiene una sección 'Criterios de aceptación' con criterios testeables sin ac_hash.\n Agregá los criterios bajo '## Criterios de aceptación', o corré /grill-user-story para pulir la US.`, 2);
102
102
  title = opts.title || extractTitle(md);
103
103
  } else {
104
104
  // Fuente tracker: traer la US del adaptador (mismo hash que usará `dai check`).
@@ -107,7 +107,7 @@ async function cmdLinkUs(key, opts) {
107
107
  const us = await adapter.fetchUS(key);
108
108
  if (!us) fail(`no encontré la US ${key} en el backend ${adapter.kind}. Pasa --us <md> o revisa el .env.`, 2);
109
109
  hash = us.ac_hash;
110
- if (hash == null) fail(`la US ${key} no tiene 'Criterios de aceptación' → sin ac_hash.`, 2);
110
+ if (hash == null) fail(`la US ${key} no tiene una sección 'Criterios de aceptación' con criterios testeables → sin ac_hash, no se puede linkear.\n Agregá la sección en el tracker, o corré /grill-user-story ${key} para pulir la US (te interroga y la re-publica).`, 2);
111
111
  title = opts.title || us.title;
112
112
  version = us.spec_version || "v1";
113
113
  }
@@ -503,7 +503,13 @@ async function askYesNo(rl, q, def = false) {
503
503
  // ── init: scaffolder interactivo del repo ─────────────────────────────────────
504
504
  async function cmdInit(repo, opts) {
505
505
  repo = repo || "."; // por defecto, el directorio actual (como git init / npm init)
506
- if (!existsSync(repo)) fail(`no existe el directorio: ${repo}`);
506
+ if (!existsSync(repo)) {
507
+ // Error común: `--for claude, cursor` con espacio → la shell parte y 'cursor' cae acá como repo.
508
+ const hint = isAssistantToken(repo)
509
+ ? `\n ¿Separaste --for con un espacio? '${repo}' quedó como <repo>. Usá coma SIN espacio: --for claude,cursor`
510
+ : "";
511
+ fail(`no existe el directorio: ${repo}${hint}`);
512
+ }
507
513
  const rl = process.stdin.isTTY ? createInterface({ input: process.stdin, output: process.stdout }) : null;
508
514
 
509
515
  process.stdout.write("\n dai · configurar este repo para desarrollo asistido por IA\n");
@@ -557,8 +563,9 @@ async function cmdInit(repo, opts) {
557
563
  writeFileSync(join(dai, "VERSION"), readFileSync(join(ROOT, "VERSION"), "utf8"));
558
564
  ok(".dai/ moldes (templates) + reglas (governance) del método");
559
565
 
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");
566
+ // .env.example — aditivo, reflejando el --pm elegido: mismas claves que el .env
567
+ // (con valores VACÍOS, sin secretos) para que el token del tracker esté presente.
568
+ const exSrc = envFor(pm);
562
569
  const exPath = join(repo, ".env.example");
563
570
  if (existsSync(exPath)) {
564
571
  const cur = readFileSync(exPath, "utf8"), merged = mergeEnv(cur, exSrc);
package/cli/lib/args.mjs CHANGED
@@ -5,6 +5,12 @@
5
5
 
6
6
  export const camel = (s) => s.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
7
7
 
8
+ // Tokens válidos de `--for`. Sirve para detectar el error común de separar la lista
9
+ // con un espacio (`--for claude, cursor`): la shell parte antes de que dai lo vea, y
10
+ // `cursor` cae como posicional (el `<repo>`). Con esto damos un hint claro.
11
+ const ASSISTANT_TOKENS = new Set(["claude", "copilot", "cursor", "both", "all"]);
12
+ export const isAssistantToken = (s) => ASSISTANT_TOKENS.has(String(s ?? "").toLowerCase());
13
+
8
14
  // Parsea el valor de `--for` como una LISTA COMBINABLE de asistentes.
9
15
  // Acepta "claude", "copilot", "cursor" (combinables con coma o espacio),
10
16
  // "both" (= claude+copilot) y "all" (= los tres). Lanza si hay un token inválido.
@@ -0,0 +1,122 @@
1
+ # ADR-0010 — Versionado y upgrade (compatibilidad, `doctor` version-drift, `dai sync`)
2
+
3
+ - **Estado:** propuesto
4
+ - **Fecha:** 2026-07-09
5
+ - **Decide:** lead / arquitecto de la metodología
6
+
7
+ ## Contexto
8
+
9
+ Un equipo scaffoldea su repo con dai `X.Y.Z`. Después publicamos versiones nuevas
10
+ (`2.2`, `2.9`, `3.0`). ¿Qué pasa con su trabajo? ¿Tienen que actualizar el CLI y el
11
+ repo? Hoy no hay política escrita ni herramienta de upgrade — el equipo queda a ciegas.
12
+
13
+ Lo que dai deja en un repo no es una cosa, son **cuatro capas** con impacto distinto
14
+ ante un upgrade (y una premisa de fondo: dai **da herramientas, no obliga** — MANIFIESTO
15
+ Art. 14, "la ceremonia se agrega cuando duele, no antes"):
16
+
17
+ | Capa | Qué es | Naturaleza |
18
+ |---|---|---|
19
+ | **CLI** | el binario `dai` (npm global) | por máquina/dev |
20
+ | **Copias scaffoldeadas** | skills, constitución (`CLAUDE.md`/rules), templates, PR template | **caché derivable**, commiteado por repo |
21
+ | **OpenSpec** | `openspec/` | **herramienta aparte** (@fission-ai/openspec), versionado propio |
22
+ | **Dato de trazabilidad** | `implements.yaml` (id, version, **`ac_hash`**) | el **contrato** (ADR-0001, ADR-0004) |
23
+
24
+ Solo la última capa, si cambia, **invalida trabajo hecho**. Por eso el principio rector
25
+ es: **dai no obliga a actualizar** — `doctor` avisa, el equipo decide cuándo.
26
+
27
+ ## Decisión
28
+
29
+ ### 1. La compatibilidad la comunica el semver (formaliza `RELEASING.md`)
30
+
31
+ - **patch / minor** (`2.0` → `2.2` → `2.9`): el **contrato NO cambia**. Todo lo hecho
32
+ sigue válido; `dai check` da igual que antes. Upgrade **opcional** (fixes/features).
33
+ - **major** (`2.x` → `3.0`): puede cambiar el algoritmo del `ac_hash` o el schema del
34
+ `implements.yaml`. Requiere **migración**.
35
+
36
+ **Invariante duro:** *dentro de una línea major, `ac_hash(input)` es estable* — el hash
37
+ que calcula `2.0` es idéntico al de `2.9` para la misma US. Es lo que vuelve seguros a
38
+ los minors. Se blinda con **golden vectors de `ac_hash` en CI que NO pueden cambiar
39
+ dentro de una major** (romperlos = obligado a bumpear major).
40
+
41
+ ### 2. Las copias son caché derivable, no algo que se mantiene a mano
42
+
43
+ Las skills/constitución/templates del repo son **copias** de lo que trae el CLI. Una
44
+ copia vieja **sigue funcionando** (una skill/constitución vieja es un prompt válido).
45
+ No se editan a mano en el repo: se **regeneran** (mismo espíritu que "la cobertura se
46
+ deriva"). La fuente de verdad es la versión del CLI.
47
+
48
+ OpenSpec queda **fuera de alcance**: es otra herramienta con su propio upgrade
49
+ (`openspec`), dai solo la instala/inicia. `dai sync` **no** toca OpenSpec.
50
+
51
+ ### 3. `.dai/VERSION` = provenance del scaffold
52
+
53
+ `dai init` ya estampa con qué versión se scaffoldeó el repo. Es el marcador para
54
+ detectar drift entre el repo y el CLI instalado.
55
+
56
+ ### 4. `dai doctor` chequea version-drift
57
+
58
+ Compara `.dai/VERSION` (repo) contra la versión del CLI instalado y reporta:
59
+
60
+ | Situación | Mensaje |
61
+ |---|---|
62
+ | iguales | ✓ al día |
63
+ | CLI > repo, **misma major** | ℹ️ refresh disponible (skills/constitución de `vX`, CLI `vY`). `dai sync` cuando quieras — **opcional, nada roto** |
64
+ | CLI **major** > repo | ⚠️ cambio mayor: puede tocar el contrato. Ver `MIGRATION.md`/CHANGELOG antes; `dai sync` + `dai migrate` si aplica |
65
+ | CLI < repo | ⚠️ tu repo se scaffoldeó con una versión más nueva que tu CLI — actualizá el CLI |
66
+
67
+ ### 5. `dai sync` — refresco **aditivo** de las copias (comando nuevo)
68
+
69
+ Re-genera skills, constitución (bloque `<!-- dai:start/end -->`), templates y PR
70
+ template **a la versión del CLI**, **aditivo e idempotente**. Reusa la maquinaria del
71
+ init aditivo: `upsertBlock` (constitución), `mergeEnv` (`.env`/`.env.example`),
72
+ `reconcileGitignore`. **No pisa** lo del proyecto (constitución propia, config). Respeta
73
+ el `--for` ya presente en el repo y actualiza `.dai/VERSION`.
74
+
75
+ - Flags: `--dry-run` (qué cambiaría, sin tocar), `--for` (override de asistentes).
76
+ - Es **opt-in**: lo corre el equipo cuando quiere.
77
+ - Relación con `dai install`: `sync` = `install --local . --force` **consciente de la
78
+ constitución + templates + `.dai/VERSION`**, con reporte de drift. Se puede
79
+ implementar como extensión/alias de `install`, pero el verbo explícito comunica mejor
80
+ la intención (refrescar un repo ya inicializado).
81
+
82
+ ### 6. Majors: `dai migrate` + `MIGRATION.md` + nota BREAKING
83
+
84
+ Cuando un major cambia el contrato, se provee migración — p. ej. re-derivar y
85
+ re-estampar los `ac_hash` en masa (un `link-us --resync` para todos los
86
+ `implements.yaml`). Documentado en `MIGRATION.md`, marcado **BREAKING** en el CHANGELOG.
87
+ Los majors son **raros y bien soportados**.
88
+
89
+ ### 7. Future-proofing del schema (enmienda menor a ADR-0004)
90
+
91
+ Estampar `schema: <n>` en el `implements.yaml` para que un futuro `dai migrate` sepa de
92
+ qué versión de schema migrar. Se puede **diferir** hasta que un major lo necesite, pero
93
+ conviene reservar el campo desde ya.
94
+
95
+ ## Consecuencias
96
+
97
+ - ✅ El **número de versión comunica el radio de explosión**: el equipo sabe si un
98
+ upgrade es seguro sin leer diffs.
99
+ - ✅ Upgrades **opt-in** — respeta el ADN (dai no obliga). `doctor` avisa, el equipo
100
+ decide.
101
+ - ✅ Las copias son caché → `dai sync` las refresca sin miedo (aditivo, no pisa).
102
+ - ✅ **Reusa** la maquinaria aditiva ya construida y testeada (`upsertBlock`/`mergeEnv`/
103
+ `reconcileGitignore`, v0.3.0) — costo de implementación bajo.
104
+ - ⚠️ Obliga a **golden vectors de `ac_hash` en CI** inmutables dentro de una major —
105
+ disciplina de release.
106
+ - ⚠️ `dai sync` debe ser **estrictamente aditivo/idempotente**; un bug ahí pisaría
107
+ trabajo (mismo riesgo que tuvo el `init` — ya mitigado y testeado).
108
+ - ⚠️ Hay que comunicar que `dai sync` **no** actualiza OpenSpec (upgrade aparte).
109
+
110
+ ## Alternativas consideradas
111
+
112
+ - **Forzar re-scaffold/upgrade en cada versión** — descartado: viola el ADN (dai no
113
+ obliga); rompería la adopción. dai es como git: no te dicta cómo trabajar.
114
+ - **No versionar el scaffold (sin drift check)** — descartado: sin provenance no se
115
+ puede avisar; el equipo queda a ciegas.
116
+ - **Mantener las copias a mano** — descartado: contradice "la cobertura se deriva"; las
117
+ copias son caché, la fuente es el CLI.
118
+ - **`dai sync` destructivo (pisar y re-copiar)** — descartado: pisaría la constitución
119
+ propia del proyecto. Debe ser aditivo (lección del `init`, v0.3.0).
120
+ - **Meter todo en `dai install --force`** — posible; pero `sync` como verbo explícito +
121
+ reporte de drift comunica mejor la intención. Se implementa como extensión de
122
+ `install`.
@@ -15,6 +15,7 @@ decisión cambia, se escribe un ADR nuevo que supersede al viejo. Molde en
15
15
  | [0007](0007-modelo-de-autenticacion.md) | Modelo de auth: SSH para git, tokens scopeados para forge/tracker, sin contraseñas | aceptado |
16
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
17
  | [0009](0009-adaptador-cursor.md) | Adaptador nativo para Cursor (skills + rules) con `dai init`/`install`/`doctor` | propuesto |
18
+ | [0010](0010-versionado-y-upgrade.md) | Versionado y upgrade: compatibilidad por semver, `doctor` version-drift, `dai sync` aditivo | propuesto |
18
19
 
19
20
  > Estas son las decisiones que cierran las "Decisiones abiertas" de
20
21
  > [`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.3.0",
3
+ "version": "0.3.1",
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/",
@@ -127,3 +127,44 @@ Lo que quedó sin resolver y necesita una decisión antes de pasar a implementac
127
127
  - spec_version (número legible) lo sube el PO/skill para comunicar; ac_hash lo
128
128
  calcula el CI para detectar. El número comunica, el hash detecta.
129
129
  -->
130
+
131
+ ---
132
+
133
+ <!--
134
+ ══════════════════════════════════════════════════════════════════════════════
135
+ EJEMPLO LISTO PARA PROBAR — copiá desde el título de abajo hasta el final.
136
+ Es lo MÍNIMO que funciona (una US real necesita más secciones, ver arriba),
137
+ pero alcanza para ver el flujo:
138
+
139
+ • Sin tracker: guardalo como .dai/us/EJ-1.md y corré:
140
+ dai ac-hash .dai/us/EJ-1.md # imprime el ac_hash
141
+ dai link-us EJ-1 --us .dai/us/EJ-1.md --dry-run # muestra branch + implements.yaml
142
+ • Con tracker: pegalo como una US nueva y corré dai link-us <ID>.
143
+
144
+ La clave es la sección "Criterios de aceptación": es lo que se hashea. Sin ella,
145
+ dai no linkea (por diseño: no hay link sin criterios testeables).
146
+ ══════════════════════════════════════════════════════════════════════════════
147
+ -->
148
+
149
+ # Marcar una tarea como completada
150
+
151
+ ## Historia
152
+
153
+ Como **usuario de la lista de tareas**
154
+ quiero **marcar una tarea como completada**
155
+ para **distinguir de un vistazo lo que ya hice de lo que me falta**.
156
+
157
+ ## Criterios de aceptación
158
+
159
+ - [ ] **AC-1** —
160
+ - **Dado** una tarea pendiente en mi lista
161
+ - **Cuando** la marco como completada
162
+ - **Entonces** queda diferenciada como hecha y deja de contar en el total de pendientes.
163
+ - [ ] **AC-2** —
164
+ - **Dado** una tarea ya completada
165
+ - **Cuando** la vuelvo a marcar
166
+ - **Entonces** vuelve al estado pendiente y se recuenta en el total.
167
+ - [ ] **AC-3** —
168
+ - **Dado** que recargo la página
169
+ - **Cuando** vuelve a cargar mi lista
170
+ - **Entonces** cada tarea conserva el estado (completada o pendiente) que tenía.