@dforce2055/dai 0.3.0 → 0.4.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 +47 -0
- package/README.md +9 -1
- package/VERSION +1 -1
- package/cli/dai.mjs +132 -7
- package/cli/lib/args.mjs +6 -0
- package/cli/lib/semver.mjs +30 -0
- package/docs/adr/0010-versionado-y-upgrade.md +122 -0
- package/docs/adr/README.md +1 -0
- package/package.json +1 -1
- package/templates/formato-us.md +41 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,51 @@
|
|
|
3
3
|
Formato basado en [Keep a Changelog](https://keepachangelog.com/). Versionado semver
|
|
4
4
|
(ver `VERSION`).
|
|
5
5
|
|
|
6
|
+
## [0.4.0] — 2026-07-10
|
|
7
|
+
|
|
8
|
+
**Versionado y upgrade** ([ADR-0010](docs/adr/0010-versionado-y-upgrade.md)): mantené tu repo al
|
|
9
|
+
día con el CLI sin pisar nada. Las copias scaffoldeadas (skills, constitución, templates) son un
|
|
10
|
+
caché derivable — ahora la máquina te avisa cuando quedaron atrás y las refresca sola.
|
|
11
|
+
|
|
12
|
+
### Agregado
|
|
13
|
+
- **`dai sync`**: refresca skills, constitución, templates y PR template a la versión del CLI —
|
|
14
|
+
**aditivo** (conserva tu `CLAUDE.md` propio vía bloque delimitado), sin tocar el `.env` ni OpenSpec.
|
|
15
|
+
Detecta los asistentes del repo o acepta `--for`; `--dry-run` muestra qué cambiaría.
|
|
16
|
+
- **`dai doctor` · version-drift**: compara `.dai/VERSION` (scaffold del repo) vs el CLI y avisa con
|
|
17
|
+
color + `⬆️` (misma major → refresh opcional con `dai sync`; major distinta → revisar migración;
|
|
18
|
+
repo más nuevo → actualizar el CLI).
|
|
19
|
+
- **`dai version`**: además de la versión, muestra el estado de drift si estás en un repo con dai
|
|
20
|
+
(chequeo liviano). `dai --version` fuera de un repo dai queda limpio (solo la versión).
|
|
21
|
+
- **`lib/semver.mjs`** (comparación de versiones, cero dependencias).
|
|
22
|
+
|
|
23
|
+
### Interno
|
|
24
|
+
- **Golden vectors de `ac_hash`**: pineados como inmutables dentro de la línea major — blindan el
|
|
25
|
+
contrato ([ADR-0001](docs/adr/0001-contrato-ac-hash.md)) que hace seguros a los minors/patches y a `dai sync`.
|
|
26
|
+
- **109 tests** (+4 desde 0.3.1: semver ×3, golden vectors ×1).
|
|
27
|
+
|
|
28
|
+
> Diferido a un futuro major (ya diseñado en el ADR-0010): `dai migrate` + `MIGRATION.md` y estampar
|
|
29
|
+
> `schema:` en el `implements.yaml`.
|
|
30
|
+
|
|
31
|
+
## [0.3.1] — 2026-07-10
|
|
32
|
+
|
|
33
|
+
Pulido de la experiencia de `dai init` y `dai link-us`, y un ejemplo de US listo para probar.
|
|
34
|
+
|
|
35
|
+
### Corregido
|
|
36
|
+
- **`dai init` · `.env.example`**: ahora refleja el `--pm` elegido — incluye todas las claves del
|
|
37
|
+
tracker (con el `..._TOKEN`), en vez del template genérico `md`. Antes, al elegir jira/clickup,
|
|
38
|
+
el `.env.example` quedaba con `DAI_PM=md` y sin el token.
|
|
39
|
+
- **`dai init` · `--for` con espacio**: mensaje claro cuando `--for claude, cursor` (con espacio) hace
|
|
40
|
+
que la shell parta la lista y un token de asistente caiga como `<repo>` ("no existe el directorio: cursor").
|
|
41
|
+
- **`dai link-us` · `dai ac-hash`**: mensaje accionable cuando la US no tiene sección
|
|
42
|
+
'Criterios de aceptación' — sugiere agregarla o correr `/grill-user-story <ID>`.
|
|
43
|
+
|
|
44
|
+
### Agregado
|
|
45
|
+
- **`templates/formato-us.md`**: ejemplo de US copy-paste al final (con criterios testeables) para
|
|
46
|
+
probar el flujo en 30 segundos, con o sin tracker (`dai ac-hash` / `dai link-us --us … --dry-run`).
|
|
47
|
+
|
|
48
|
+
### Interno
|
|
49
|
+
- **105 tests** (+1 desde 0.3.0: `isAssistantToken`).
|
|
50
|
+
|
|
6
51
|
## [0.3.0] — 2026-07-08
|
|
7
52
|
|
|
8
53
|
`dai init` ahora es **aditivo**: no pisa la configuración de un repo funcional. Más
|
|
@@ -102,6 +147,8 @@ ClickUp y Jira Cloud.
|
|
|
102
147
|
- Tests de las rutas de red (jira/clickup/forge) con `fetch` mockeado. Sin links rotos;
|
|
103
148
|
`files` de npm sin tests ni secretos.
|
|
104
149
|
|
|
150
|
+
[0.4.0]: https://github.com/dforce2055/dai/releases/tag/v0.4.0
|
|
151
|
+
[0.3.1]: https://github.com/dforce2055/dai/releases/tag/v0.3.1
|
|
105
152
|
[0.3.0]: https://github.com/dforce2055/dai/releases/tag/v0.3.0
|
|
106
153
|
[0.2.0]: https://github.com/dforce2055/dai/releases/tag/v0.2.0
|
|
107
154
|
[0.1.1]: https://github.com/dforce2055/dai/releases/tag/v0.1.1
|
package/README.md
CHANGED
|
@@ -158,6 +158,7 @@ flowchart TD
|
|
|
158
158
|
|---|---|
|
|
159
159
|
| `dai init [<repo>]` | scaffolder interactivo del repo. Flags: `--for claude\|copilot\|both\|cursor\|all` (asistente, default `all`) · `--pm md\|jira\|clickup` (tracker) · `--openspec` |
|
|
160
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
|
+
| `dai sync [--dry-run] [--for <asistentes>]` | **refresca** skills, constitución, templates y PR template a la versión del CLI — **aditivo** (no pisa tu `CLAUDE.md`), no toca el `.env` ni OpenSpec. Detecta los asistentes del repo o pasás `--for`. `--dry-run` muestra qué cambiaría ([ADR-0010](docs/adr/0010-versionado-y-upgrade.md)) |
|
|
161
162
|
| `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
163
|
| `dai link-us <ID> [--us <md>]` | crea branch + `implements.yaml`; sin `--us` trae la US del tracker |
|
|
163
164
|
| `dai link-us <ID> --resync` | re-estampa el `ac_hash` contra la US viva (tras un ⚠️ de check) |
|
|
@@ -168,7 +169,14 @@ flowchart TD
|
|
|
168
169
|
| `dai done [--base main] [--force]` | cierra la US: vuelve a la base, `fetch --prune` + `pull`, y borra la branch local **si está mergeada** (chequeo estricto; `--force` la borra igual). Redes: no estar en la base, sin cambios sueltos, sin commits sin pushear |
|
|
169
170
|
| `dai forge comment <ref> --body-file <f>` · `dai forge pr <ref>` | comentar / leer una PR/MR (GitHub/GitLab) |
|
|
170
171
|
| `dai ac-hash <us.md>` | calcula el hash de los criterios de aceptación de una US |
|
|
171
|
-
| `dai doctor` · `dai docs <dest>` · `dai
|
|
172
|
+
| `dai doctor` · `dai docs <dest>` · `dai version` | diagnóstico del entorno (incluye **version-drift** del scaffold) · copiar la doc · versión (`dai version` avisa si tu repo quedó atrás) |
|
|
173
|
+
|
|
174
|
+
> **🆕 Mantené tu repo al día — `dai sync`.** Las skills, la constitución y los templates son un
|
|
175
|
+
> *caché derivable* del CLI. Cuando actualizás `dai` (`npm i -g @dforce2055/dai`), **`dai doctor` y
|
|
176
|
+
> `dai version` te avisan solos** si tu scaffold quedó atrás — con color y un `⬆️` —, y **`dai sync`**
|
|
177
|
+
> lo refresca: **aditivo** (conserva tu `CLAUDE.md` propio), sin tocar el `.env` ni OpenSpec. Probá sin
|
|
178
|
+
> riesgo con `dai sync --dry-run`. El versionado es semver: patch/minor no rompen nada; solo un major
|
|
179
|
+
> pediría migración. ([ADR-0010](docs/adr/0010-versionado-y-upgrade.md))
|
|
172
180
|
|
|
173
181
|
Skills (se invocan en el asistente): `/doc-to-backlog` · `/grill-intent` · `/grill-epic` · `/grill-user-story` · `/link-us` ·
|
|
174
182
|
`/tdd` · `/dai-review`. Config del tracker (`md`\|`jira`\|`clickup`) y tokens: en `.env` —
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.4.0
|
package/cli/dai.mjs
CHANGED
|
@@ -27,7 +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, parseAssistants } from "./lib/args.mjs";
|
|
30
|
+
import { parseFlags, parseAssistants, isAssistantToken } from "./lib/args.mjs";
|
|
31
|
+
import { versionDrift } from "./lib/semver.mjs";
|
|
31
32
|
import { skillToPrompt, skillToCursor, constitution, constitutionCursorRule, envFor, mergeEnv, upsertBlock, reconcileGitignore } from "./lib/bootstrap.mjs";
|
|
32
33
|
|
|
33
34
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
@@ -39,6 +40,10 @@ function fail(msg, code = 1) { process.stderr.write("dai: " + msg + "\n"); proce
|
|
|
39
40
|
const ok = (m) => process.stdout.write(`✓ ${m}\n`);
|
|
40
41
|
const info = (m) => process.stdout.write(`› ${m}\n`);
|
|
41
42
|
const warn = (m) => process.stdout.write(`⚠ ${m}\n`);
|
|
43
|
+
// Color ANSI mínimo — solo si es TTY y no está NO_COLOR (así no ensucia pipes/CI).
|
|
44
|
+
const _color = process.stdout.isTTY && !process.env.NO_COLOR;
|
|
45
|
+
const paint = (code, m) => (_color ? `\x1b[${code}m${m}\x1b[0m` : m);
|
|
46
|
+
const C = { y: (m) => paint("33", m), r: (m) => paint("31", m), cy: (m) => paint("36", m), b: (m) => paint("1", m) };
|
|
42
47
|
const ROOT = join(HERE, ".."); // raíz del paquete dai (cli/ está adentro)
|
|
43
48
|
const CLAUDE_SKILLS_DIR = process.env.CLAUDE_SKILLS_DIR || join(homedir(), ".claude", "skills");
|
|
44
49
|
const CURSOR_SKILLS_DIR = process.env.CURSOR_SKILLS_DIR || join(homedir(), ".cursor", "skills");
|
|
@@ -59,7 +64,7 @@ function trackerUrl(id) {
|
|
|
59
64
|
function cmdAcHash(arg) {
|
|
60
65
|
const md = arg ? readFileSync(arg, "utf8") : readFileSync(0, "utf8");
|
|
61
66
|
const h = acHash(md);
|
|
62
|
-
if (h == null) fail("la US no tiene bloque de 'Criterios de aceptación'.", 2);
|
|
67
|
+
if (h == null) fail("la US no tiene un bloque 'Criterios de aceptación' (criterios testeables bajo '## Criterios de aceptación').", 2);
|
|
63
68
|
process.stdout.write(h + "\n");
|
|
64
69
|
}
|
|
65
70
|
|
|
@@ -98,7 +103,7 @@ async function cmdLinkUs(key, opts) {
|
|
|
98
103
|
// Fuente local: un .md con la US.
|
|
99
104
|
const md = readFileSync(opts.us, "utf8");
|
|
100
105
|
hash = acHash(md);
|
|
101
|
-
if (hash == null) fail(
|
|
106
|
+
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
107
|
title = opts.title || extractTitle(md);
|
|
103
108
|
} else {
|
|
104
109
|
// Fuente tracker: traer la US del adaptador (mismo hash que usará `dai check`).
|
|
@@ -107,7 +112,7 @@ async function cmdLinkUs(key, opts) {
|
|
|
107
112
|
const us = await adapter.fetchUS(key);
|
|
108
113
|
if (!us) fail(`no encontré la US ${key} en el backend ${adapter.kind}. Pasa --us <md> o revisa el .env.`, 2);
|
|
109
114
|
hash = us.ac_hash;
|
|
110
|
-
if (hash == null) fail(`la US ${key} no tiene 'Criterios de aceptación' → sin ac_hash.`, 2);
|
|
115
|
+
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
116
|
title = opts.title || us.title;
|
|
112
117
|
version = us.spec_version || "v1";
|
|
113
118
|
}
|
|
@@ -503,7 +508,13 @@ async function askYesNo(rl, q, def = false) {
|
|
|
503
508
|
// ── init: scaffolder interactivo del repo ─────────────────────────────────────
|
|
504
509
|
async function cmdInit(repo, opts) {
|
|
505
510
|
repo = repo || "."; // por defecto, el directorio actual (como git init / npm init)
|
|
506
|
-
if (!existsSync(repo))
|
|
511
|
+
if (!existsSync(repo)) {
|
|
512
|
+
// Error común: `--for claude, cursor` con espacio → la shell parte y 'cursor' cae acá como repo.
|
|
513
|
+
const hint = isAssistantToken(repo)
|
|
514
|
+
? `\n ¿Separaste --for con un espacio? '${repo}' quedó como <repo>. Usá coma SIN espacio: --for claude,cursor`
|
|
515
|
+
: "";
|
|
516
|
+
fail(`no existe el directorio: ${repo}${hint}`);
|
|
517
|
+
}
|
|
507
518
|
const rl = process.stdin.isTTY ? createInterface({ input: process.stdin, output: process.stdout }) : null;
|
|
508
519
|
|
|
509
520
|
process.stdout.write("\n dai · configurar este repo para desarrollo asistido por IA\n");
|
|
@@ -557,8 +568,9 @@ async function cmdInit(repo, opts) {
|
|
|
557
568
|
writeFileSync(join(dai, "VERSION"), readFileSync(join(ROOT, "VERSION"), "utf8"));
|
|
558
569
|
ok(".dai/ moldes (templates) + reglas (governance) del método");
|
|
559
570
|
|
|
560
|
-
// .env.example — aditivo
|
|
561
|
-
|
|
571
|
+
// .env.example — aditivo, reflejando el --pm elegido: mismas claves que el .env
|
|
572
|
+
// (con valores VACÍOS, sin secretos) para que el token del tracker esté presente.
|
|
573
|
+
const exSrc = envFor(pm);
|
|
562
574
|
const exPath = join(repo, ".env.example");
|
|
563
575
|
if (existsSync(exPath)) {
|
|
564
576
|
const cur = readFileSync(exPath, "utf8"), merged = mergeEnv(cur, exSrc);
|
|
@@ -670,6 +682,113 @@ function cmdDocs(dest) {
|
|
|
670
682
|
ok(`documentación copiada a ${dest}`);
|
|
671
683
|
}
|
|
672
684
|
|
|
685
|
+
// ── sync: refresca las copias scaffoldeadas a la versión del CLI (ADR-0010) ───
|
|
686
|
+
// Las copias (skills, constitución, templates, PR template) son un CACHÉ derivable
|
|
687
|
+
// del CLI: `dai sync` las re-genera a la versión instalada, aditivo (no pisa la
|
|
688
|
+
// constitución propia del proyecto). NO toca el `.env` ni OpenSpec. Opt-in.
|
|
689
|
+
function cmdSync(repo, opts) {
|
|
690
|
+
repo = repo || ".";
|
|
691
|
+
if (!existsSync(repo)) fail(`no existe el directorio: ${repo}`, 2);
|
|
692
|
+
const daiDir = join(repo, ".dai");
|
|
693
|
+
if (!existsSync(daiDir)) fail("este repo no tiene dai (falta .dai/). Corré `dai init` primero.", 2);
|
|
694
|
+
|
|
695
|
+
// Asistentes: --for override, o detectar los que ya están en el repo.
|
|
696
|
+
let want;
|
|
697
|
+
if (typeof opts.for === "string") {
|
|
698
|
+
try { want = parseAssistants(opts.for); } catch (e) { fail(`--for ${e.message}`); }
|
|
699
|
+
} else {
|
|
700
|
+
want = {
|
|
701
|
+
claude: existsSync(join(repo, ".claude", "skills")),
|
|
702
|
+
copilot: existsSync(join(repo, ".github", "prompts")),
|
|
703
|
+
cursor: existsSync(join(repo, ".cursor", "skills")),
|
|
704
|
+
};
|
|
705
|
+
if (!want.claude && !want.copilot && !want.cursor)
|
|
706
|
+
fail("no detecté asistentes instalados (.claude/.cursor/.github/prompts). Pasá --for.", 2);
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
const cliV = readFileSync(join(ROOT, "VERSION"), "utf8").trim();
|
|
710
|
+
const repoV = existsSync(join(daiDir, "VERSION")) ? readFileSync(join(daiDir, "VERSION"), "utf8").trim() : "?";
|
|
711
|
+
const dry = !!opts.dryRun;
|
|
712
|
+
const forStr = [want.claude && "claude", want.copilot && "copilot", want.cursor && "cursor"].filter(Boolean).join("+");
|
|
713
|
+
info(`dai sync — ${repoV} → v${cliV} · asistentes: ${forStr}${dry ? " [dry-run]" : ""}`);
|
|
714
|
+
|
|
715
|
+
const skillsSrc = join(ROOT, "skills");
|
|
716
|
+
const skills = readdirSync(skillsSrc).filter((n) => statSync(join(skillsSrc, n)).isDirectory());
|
|
717
|
+
const step = (label, fn) => { if (dry) info(`[dry-run] ${label}`); else { fn(); ok(label); } };
|
|
718
|
+
|
|
719
|
+
// 1. .dai/ (templates + governance) + VERSION
|
|
720
|
+
step(`.dai/ moldes + governance → v${cliV}`, () => {
|
|
721
|
+
for (const sub of ["templates", "governance"]) {
|
|
722
|
+
const src = join(ROOT, sub);
|
|
723
|
+
if (existsSync(src)) { mkdirSync(join(daiDir, sub), { recursive: true }); cpSync(src, join(daiDir, sub), { recursive: true }); }
|
|
724
|
+
}
|
|
725
|
+
writeFileSync(join(daiDir, "VERSION"), readFileSync(join(ROOT, "VERSION"), "utf8"));
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
// 2. PR template
|
|
729
|
+
step(".github/ pull_request_template.md", () => {
|
|
730
|
+
mkdirSync(join(repo, ".github"), { recursive: true });
|
|
731
|
+
cpSync(join(ROOT, "templates", "pull-request.md"), join(repo, ".github", "pull_request_template.md"));
|
|
732
|
+
});
|
|
733
|
+
|
|
734
|
+
// 3. skills + constitución por asistente (aditivo: upsertBlock no pisa lo del proyecto)
|
|
735
|
+
if (want.claude) step(`Claude: .claude/skills/ (${skills.length}) + CLAUDE.md (bloque dai)`, () => {
|
|
736
|
+
const dir = join(repo, ".claude", "skills"); mkdirSync(dir, { recursive: true });
|
|
737
|
+
for (const name of skills) cpSync(join(skillsSrc, name), join(dir, name), { recursive: true });
|
|
738
|
+
const cPath = join(repo, "CLAUDE.md"), cCur = existsSync(cPath) ? readFileSync(cPath, "utf8") : "";
|
|
739
|
+
writeFileSync(cPath, upsertBlock(cCur, constitution("claude")));
|
|
740
|
+
});
|
|
741
|
+
if (want.copilot) step(`Copilot: .github/prompts/ (${skills.length}) + copilot-instructions.md`, () => {
|
|
742
|
+
const pdir = join(repo, ".github", "prompts"); mkdirSync(pdir, { recursive: true });
|
|
743
|
+
for (const name of skills) writeFileSync(join(pdir, `${name}.prompt.md`), skillToPrompt(readFileSync(join(skillsSrc, name, "SKILL.md"), "utf8")));
|
|
744
|
+
const ciPath = join(repo, ".github", "copilot-instructions.md"), ciCur = existsSync(ciPath) ? readFileSync(ciPath, "utf8") : "";
|
|
745
|
+
writeFileSync(ciPath, upsertBlock(ciCur, constitution("copilot")));
|
|
746
|
+
});
|
|
747
|
+
if (want.cursor) step(`Cursor: .cursor/skills/ (${skills.length}) + .cursor/rules/dai-constitution.mdc`, () => {
|
|
748
|
+
const dir = join(repo, ".cursor", "skills"); mkdirSync(dir, { recursive: true });
|
|
749
|
+
for (const name of skills) {
|
|
750
|
+
const src = join(skillsSrc, name), target = join(dir, name);
|
|
751
|
+
cpSync(src, target, { recursive: true });
|
|
752
|
+
writeFileSync(join(target, "SKILL.md"), skillToCursor(readFileSync(join(src, "SKILL.md"), "utf8")));
|
|
753
|
+
}
|
|
754
|
+
mkdirSync(join(repo, ".cursor", "rules"), { recursive: true });
|
|
755
|
+
writeFileSync(join(repo, ".cursor", "rules", "dai-constitution.mdc"), constitutionCursorRule());
|
|
756
|
+
});
|
|
757
|
+
|
|
758
|
+
// 4. .gitignore: versiona los artefactos, deja fuera solo lo personal
|
|
759
|
+
const giPath = join(repo, ".gitignore");
|
|
760
|
+
const gi = reconcileGitignore(existsSync(giPath) ? readFileSync(giPath, "utf8") : "", want);
|
|
761
|
+
if (gi.changed) step(".gitignore ajustado (artefactos versionados; settings.local.json fuera)",
|
|
762
|
+
() => writeFileSync(giPath, gi.text.endsWith("\n") ? gi.text : gi.text + "\n"));
|
|
763
|
+
|
|
764
|
+
if (dry) info("dry-run: nada escrito. Quitá --dry-run para aplicar.");
|
|
765
|
+
else { process.stdout.write("\n"); ok(`sync completo — .dai/ ahora en v${cliV}`); process.stdout.write(" (El .env y OpenSpec no se tocan: OpenSpec se actualiza aparte con `openspec`.)\n"); }
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
// Imprime el estado de version-drift del scaffold (ADR-0010) con color + ícono.
|
|
769
|
+
// Reutilizado por `dai doctor` y `dai version`. Devuelve el estado, o null si el
|
|
770
|
+
// directorio no tiene dai (.dai/VERSION). No imprime nada en ese caso.
|
|
771
|
+
function reportDrift(repo = process.cwd()) {
|
|
772
|
+
const vf = join(repo, ".dai", "VERSION");
|
|
773
|
+
if (!existsSync(vf)) return null;
|
|
774
|
+
const repoV = readFileSync(vf, "utf8").trim();
|
|
775
|
+
const cliV = readFileSync(join(ROOT, "VERSION"), "utf8").trim();
|
|
776
|
+
const status = versionDrift(repoV, cliV);
|
|
777
|
+
switch (status) {
|
|
778
|
+
case "current":
|
|
779
|
+
ok(`.dai/ al día con el CLI (v${repoV})`); break;
|
|
780
|
+
case "minor-behind":
|
|
781
|
+
process.stdout.write(`${C.b(C.y("⬆️ actualización disponible"))} — CLI ${C.b("v" + cliV)}, tu repo ${repoV}. Actualizá con ${C.cy("dai sync")} · probá con ${C.cy("dai sync --dry-run")}\n`); break;
|
|
782
|
+
case "major-behind":
|
|
783
|
+
process.stdout.write(`${C.b(C.r("⚠️ cambio MAYOR"))} — CLI ${C.b("v" + cliV)}, tu repo ${repoV}. Revisá el CHANGELOG/MIGRATION antes de ${C.cy("dai sync")}\n`); break;
|
|
784
|
+
case "cli-behind":
|
|
785
|
+
process.stdout.write(`${C.b(C.y("⚠️ CLI atrasado"))} — tu repo se scaffoldeó con v${repoV}, más nuevo que tu CLI (v${cliV}). Actualizá el CLI: ${C.cy("npm i -g @dforce2055/dai")}\n`); break;
|
|
786
|
+
default:
|
|
787
|
+
warn(`.dai/ VERSION ilegible: '${repoV}'`);
|
|
788
|
+
}
|
|
789
|
+
return status;
|
|
790
|
+
}
|
|
791
|
+
|
|
673
792
|
// ── doctor: diagnóstico ───────────────────────────────────────────────────────
|
|
674
793
|
function cmdDoctor() {
|
|
675
794
|
loadEnv();
|
|
@@ -720,11 +839,15 @@ function cmdDoctor() {
|
|
|
720
839
|
process.env.DAI_CLICKUP_LIST_ID ? ok(`lista=${process.env.DAI_CLICKUP_LIST_ID} (para dai publish)`)
|
|
721
840
|
: warn("DAI_CLICKUP_LIST_ID vacío — solo hace falta para `dai publish` (crear tareas)");
|
|
722
841
|
}
|
|
842
|
+
|
|
843
|
+
// ── version-drift del scaffold vs el CLI (ADR-0010) ──────────────────────────
|
|
844
|
+
if (existsSync(join(process.cwd(), ".dai", "VERSION"))) { info("versión del scaffold:"); reportDrift(); }
|
|
723
845
|
}
|
|
724
846
|
|
|
725
847
|
// ── version ───────────────────────────────────────────────────────────────────
|
|
726
848
|
function cmdVersion() {
|
|
727
849
|
process.stdout.write(`dai v${readFileSync(join(HERE, "..", "VERSION"), "utf8").trim()}\n`);
|
|
850
|
+
reportDrift(); // en un repo con dai, avisa si el scaffold está atrasado (ADR-0010)
|
|
728
851
|
}
|
|
729
852
|
|
|
730
853
|
let [cmd, ...rest] = process.argv.slice(2);
|
|
@@ -743,6 +866,7 @@ switch (cmd) {
|
|
|
743
866
|
case "done": cmdDone(opts); break;
|
|
744
867
|
case "install": cmdInstall(opts).catch((e) => fail(String(e.message))); break;
|
|
745
868
|
case "init": cmdInit(pos[0], opts).catch((e) => fail(String(e.message))); break;
|
|
869
|
+
case "sync": cmdSync(pos[0], opts); break;
|
|
746
870
|
case "docs": cmdDocs(pos[0]); break;
|
|
747
871
|
case "doctor": cmdDoctor(); break;
|
|
748
872
|
case "version": cmdVersion(); break;
|
|
@@ -766,6 +890,7 @@ switch (cmd) {
|
|
|
766
890
|
" --for <asistentes> claude|copilot|cursor (combinables con coma) · o both|all (default all)\n" +
|
|
767
891
|
" ej: --for claude,cursor · --for copilot · --for all\n" +
|
|
768
892
|
" --pm md|jira|clickup · --openspec (con flags salteas las preguntas)\n" +
|
|
893
|
+
" sync [<repo>] [--dry-run] [--for <asistentes>] refresca skills/constitución/templates a la versión del CLI (aditivo; no toca .env ni OpenSpec)\n" +
|
|
769
894
|
" docs <destino> documentación conceptual → <destino>\n" +
|
|
770
895
|
" doctor diagnóstico del entorno\n\n" +
|
|
771
896
|
" (config: .env — ver .env.example)\n"
|
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,30 @@
|
|
|
1
|
+
// dai · comparación mínima de versiones semver (X.Y.Z). Cero dependencias.
|
|
2
|
+
// Se usa para el chequeo de version-drift (`dai doctor`) y `dai sync` (ADR-0010).
|
|
3
|
+
|
|
4
|
+
export function parseVersion(v) {
|
|
5
|
+
const m = String(v ?? "").trim().match(/^(\d+)\.(\d+)\.(\d+)/);
|
|
6
|
+
return m ? { major: Number(m[1]), minor: Number(m[2]), patch: Number(m[3]) } : null;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// -1 / 0 / 1 (a<b / a==b / a>b), o null si alguna no parsea.
|
|
10
|
+
export function compareVersions(a, b) {
|
|
11
|
+
const pa = parseVersion(a), pb = parseVersion(b);
|
|
12
|
+
if (!pa || !pb) return null;
|
|
13
|
+
const d = (pa.major - pb.major) || (pa.minor - pb.minor) || (pa.patch - pb.patch);
|
|
14
|
+
return d < 0 ? -1 : d > 0 ? 1 : 0;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Estado del scaffold del repo (repoV, de `.dai/VERSION`) frente al CLI instalado (cliV):
|
|
18
|
+
// current → iguales
|
|
19
|
+
// minor-behind → CLI adelante, MISMA major (refresh opcional con `dai sync`, nada roto)
|
|
20
|
+
// major-behind → CLI adelante, major DISTINTA (revisar CHANGELOG/MIGRATION antes)
|
|
21
|
+
// cli-behind → el repo se scaffoldeó con una versión más nueva que el CLI (actualizá el CLI)
|
|
22
|
+
// unknown → alguna versión no parseable
|
|
23
|
+
export function versionDrift(repoV, cliV) {
|
|
24
|
+
const r = parseVersion(repoV), c = parseVersion(cliV);
|
|
25
|
+
if (!r || !c) return "unknown";
|
|
26
|
+
const cmp = compareVersions(cliV, repoV);
|
|
27
|
+
if (cmp === 0) return "current";
|
|
28
|
+
if (cmp < 0) return "cli-behind";
|
|
29
|
+
return c.major > r.major ? "major-behind" : "minor-behind";
|
|
30
|
+
}
|
|
@@ -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`.
|
package/docs/adr/README.md
CHANGED
|
@@ -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
|
+
"version": "0.4.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/",
|
package/templates/formato-us.md
CHANGED
|
@@ -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.
|