@dforce2055/dai 0.4.0 → 0.5.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 +20 -0
- package/README.md +1 -0
- package/VERSION +1 -1
- package/cli/dai.mjs +37 -2
- package/cli/lib/implements.mjs +7 -2
- package/docs/adr/0011-archive-gate-de-aprobacion.md +68 -0
- package/docs/adr/README.md +1 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,25 @@
|
|
|
3
3
|
Formato basado en [Keep a Changelog](https://keepachangelog.com/). Versionado semver
|
|
4
4
|
(ver `VERSION`).
|
|
5
5
|
|
|
6
|
+
## [0.5.0] — 2026-07-10
|
|
7
|
+
|
|
8
|
+
**`archive` en el flujo** ([ADR-0011](docs/adr/0011-archive-gate-de-aprobacion.md)): cerrar el CÓMO
|
|
9
|
+
del lado de las specs canónicas, atado a la aprobación de la PR.
|
|
10
|
+
|
|
11
|
+
### Agregado
|
|
12
|
+
- **`dai archive [<change>]`**: funde los delta specs del change en las specs canónicas
|
|
13
|
+
(`openspec/specs/`) y lo archiva. Lo corre el **aprobador** de la PR, en la branch, al aprobar
|
|
14
|
+
(el fold viaja en la PR → elude la base protegida). Detecta el change activo por su `implements.yaml`
|
|
15
|
+
o le pasás el nombre; envuelve `openspec archive --yes` (mecánico → comando, no skill). Flag `--skip-specs`.
|
|
16
|
+
|
|
17
|
+
### Cambiado
|
|
18
|
+
- **`dai check` y `dai ls` saltean `openspec/changes/archive/`**: un change shippeado ya no genera
|
|
19
|
+
⚠️ de drift falso ni aparece en el listado. `discoverImplements` acepta `includeArchived` (default
|
|
20
|
+
`true`); `check`/`ls` lo pasan `false`. `stamp`/`done` mantienen el default (lo necesitan post-merge).
|
|
21
|
+
|
|
22
|
+
### Interno
|
|
23
|
+
- **110 tests** (+1 desde 0.4.0: filtro `includeArchived`).
|
|
24
|
+
|
|
6
25
|
## [0.4.0] — 2026-07-10
|
|
7
26
|
|
|
8
27
|
**Versionado y upgrade** ([ADR-0010](docs/adr/0010-versionado-y-upgrade.md)): mantené tu repo al
|
|
@@ -147,6 +166,7 @@ ClickUp y Jira Cloud.
|
|
|
147
166
|
- Tests de las rutas de red (jira/clickup/forge) con `fetch` mockeado. Sin links rotos;
|
|
148
167
|
`files` de npm sin tests ni secretos.
|
|
149
168
|
|
|
169
|
+
[0.5.0]: https://github.com/dforce2055/dai/releases/tag/v0.5.0
|
|
150
170
|
[0.4.0]: https://github.com/dforce2055/dai/releases/tag/v0.4.0
|
|
151
171
|
[0.3.1]: https://github.com/dforce2055/dai/releases/tag/v0.3.1
|
|
152
172
|
[0.3.0]: https://github.com/dforce2055/dai/releases/tag/v0.3.0
|
package/README.md
CHANGED
|
@@ -167,6 +167,7 @@ flowchart TD
|
|
|
167
167
|
| `dai pr [--assignee u] [--base b] [--draft] [--yes]` | crea TU PR/MR precargada: pregunta la branch base (default `main`), muestra el texto y confirma antes de publicar |
|
|
168
168
|
| `dai stamp` | estampa la cobertura inversa en el tracker (branch + commit-ancla) |
|
|
169
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 |
|
|
170
|
+
| `dai archive [<change>] [--skip-specs]` | **funde los delta specs del change en las specs canónicas** (`openspec/specs/`) y lo archiva. Lo corre el **aprobador** de la PR (gate de aprobación, [ADR-0011](docs/adr/0011-archive-gate-de-aprobacion.md)); detecta el change activo o le pasás el nombre. Envuelve `openspec archive` |
|
|
170
171
|
| `dai forge comment <ref> --body-file <f>` · `dai forge pr <ref>` | comentar / leer una PR/MR (GitHub/GitLab) |
|
|
171
172
|
| `dai ac-hash <us.md>` | calcula el hash de los criterios de aceptación de una US |
|
|
172
173
|
| `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) |
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.5.0
|
package/cli/dai.mjs
CHANGED
|
@@ -71,7 +71,7 @@ function cmdAcHash(arg) {
|
|
|
71
71
|
// ── ls ──────────────────────────────────────────────────────────────────────
|
|
72
72
|
function cmdLs(opts) {
|
|
73
73
|
const root = opts.root || process.cwd();
|
|
74
|
-
const found = discoverImplements(root);
|
|
74
|
+
const found = discoverImplements(root, { includeArchived: false });
|
|
75
75
|
const rows = [];
|
|
76
76
|
for (const f of found) {
|
|
77
77
|
for (const im of f.implements || []) {
|
|
@@ -170,7 +170,7 @@ function gitCommit() { try { return git(["rev-parse", "HEAD"]); } catch { return
|
|
|
170
170
|
async function cmdCheck() {
|
|
171
171
|
loadEnv();
|
|
172
172
|
const adapter = getAdapter(process.env);
|
|
173
|
-
const found = discoverImplements(process.cwd());
|
|
173
|
+
const found = discoverImplements(process.cwd(), { includeArchived: false });
|
|
174
174
|
let worst = 0, n = 0;
|
|
175
175
|
const atrasadas = [];
|
|
176
176
|
for (const f of found) for (const im of f.implements || []) {
|
|
@@ -682,6 +682,39 @@ function cmdDocs(dest) {
|
|
|
682
682
|
ok(`documentación copiada a ${dest}`);
|
|
683
683
|
}
|
|
684
684
|
|
|
685
|
+
// ── archive: funde los delta specs del change en los specs canónicos y lo archiva ─
|
|
686
|
+
// Lo corre el APROBADOR de la PR, en la branch, al aprobar: es el gate de aprobación
|
|
687
|
+
// (el fold va atado a la aprobación, no a la autoría). Envuelve `openspec archive
|
|
688
|
+
// <change> --yes` — mecánico, va al CLI y no a una skill (ADR-0002). El fold queda
|
|
689
|
+
// sin commitear para que el aprobador lo revise y lo incluya en la PR antes de mergear.
|
|
690
|
+
function cmdArchive(changeArg, opts) {
|
|
691
|
+
const repo = process.cwd();
|
|
692
|
+
if (!existsSync(join(repo, "openspec"))) fail("no hay OpenSpec en este repo (falta openspec/). Nada que archivar.", 2);
|
|
693
|
+
|
|
694
|
+
let change = changeArg;
|
|
695
|
+
if (!change) {
|
|
696
|
+
// Detectar el change ACTIVO (no archivado) por su implements.yaml.
|
|
697
|
+
const active = discoverImplements(repo, { includeArchived: false })
|
|
698
|
+
.filter((f) => /[/\\]openspec[/\\]changes[/\\]/.test(f.path))
|
|
699
|
+
.map((f) => basename(dirname(f.path)));
|
|
700
|
+
const uniq = [...new Set(active)];
|
|
701
|
+
if (uniq.length === 0) fail("no encontré un change activo para archivar (¿ya está archivado, o falta `dai link-us`?).", 2);
|
|
702
|
+
if (uniq.length > 1) fail(`hay varios changes activos: ${uniq.join(", ")}.\n Pasá cuál: dai archive <change>`, 2);
|
|
703
|
+
change = uniq[0];
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
info(`archivando '${change}' — funde los delta specs en openspec/specs/ y mueve el change a archive/…`);
|
|
707
|
+
const args = ["archive", change, "--yes"];
|
|
708
|
+
if (opts.skipSpecs) args.push("--skip-specs");
|
|
709
|
+
try {
|
|
710
|
+
execFileSync(npmBin("openspec"), args, { stdio: "inherit", cwd: repo });
|
|
711
|
+
} catch (e) {
|
|
712
|
+
fail(`openspec archive falló (¿tasks incompletas? ¿change inexistente?): ${String(e.message).split("\n")[0]}`, 1);
|
|
713
|
+
}
|
|
714
|
+
process.stdout.write("\n");
|
|
715
|
+
ok(`'${change}' archivado. Revisá los cambios (specs fundidas + change en archive/) y commitealos en la PR antes de mergear.`);
|
|
716
|
+
}
|
|
717
|
+
|
|
685
718
|
// ── sync: refresca las copias scaffoldeadas a la versión del CLI (ADR-0010) ───
|
|
686
719
|
// Las copias (skills, constitución, templates, PR template) son un CACHÉ derivable
|
|
687
720
|
// del CLI: `dai sync` las re-genera a la versión instalada, aditivo (no pisa la
|
|
@@ -864,6 +897,7 @@ switch (cmd) {
|
|
|
864
897
|
case "publish": cmdPublish(pos[0]).catch((e) => fail(String(e.message))); break;
|
|
865
898
|
case "pr": cmdPr(opts).catch((e) => fail(String(e.message))); break;
|
|
866
899
|
case "done": cmdDone(opts); break;
|
|
900
|
+
case "archive": cmdArchive(pos[0], opts); break;
|
|
867
901
|
case "install": cmdInstall(opts).catch((e) => fail(String(e.message))); break;
|
|
868
902
|
case "init": cmdInit(pos[0], opts).catch((e) => fail(String(e.message))); break;
|
|
869
903
|
case "sync": cmdSync(pos[0], opts); break;
|
|
@@ -882,6 +916,7 @@ switch (cmd) {
|
|
|
882
916
|
" check compara vs la US viva → atrasado (ADR-0003)\n" +
|
|
883
917
|
" stamp estampa la cobertura en el tracker (ADR-0005)\n" +
|
|
884
918
|
" done [--base main] [--force] cierra la US: vuelve a la base, actualiza y borra la branch local (si está mergeada)\n" +
|
|
919
|
+
" archive [<change>] [--skip-specs] funde los delta specs del change en las specs canónicas y lo archiva (lo corre el aprobador en la PR)\n" +
|
|
885
920
|
" pr [--assignee u] [--base b] [--draft] [--yes] crea TU PR/MR precargada (muestra + confirma)\n" +
|
|
886
921
|
" forge comment <ref> --body-file <f> · forge pr <ref> comentar/leer una PR ajena (github/gitlab)\n\n" +
|
|
887
922
|
"Instalación:\n" +
|
package/cli/lib/implements.mjs
CHANGED
|
@@ -71,7 +71,10 @@ export function parseImplements(text) {
|
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
// Camina el árbol desde root y devuelve { path, ...parsed } por cada implements.yaml.
|
|
74
|
-
|
|
74
|
+
// `includeArchived: false` saltea `openspec/changes/archive/` — los changes shippeados
|
|
75
|
+
// no deben aparecer en `check`/`ls` (ADR-0010). Por defecto los incluye (stamp/done los
|
|
76
|
+
// necesitan post-merge).
|
|
77
|
+
export function discoverImplements(root, { includeArchived = true } = {}) {
|
|
75
78
|
const found = [];
|
|
76
79
|
const walk = (dir) => {
|
|
77
80
|
let entries;
|
|
@@ -81,7 +84,9 @@ export function discoverImplements(root) {
|
|
|
81
84
|
let st;
|
|
82
85
|
try { st = statSync(full); } catch { continue; }
|
|
83
86
|
if (st.isDirectory()) {
|
|
84
|
-
if (
|
|
87
|
+
if (SKIP_DIRS.has(name)) continue;
|
|
88
|
+
if (!includeArchived && name === "archive") continue;
|
|
89
|
+
walk(full);
|
|
85
90
|
} else if (name === "implements.yaml") {
|
|
86
91
|
try {
|
|
87
92
|
found.push({ path: full, ...parseImplements(readFileSync(full, "utf8")) });
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# ADR-0011 — `archive` es un gate de aprobación (`dai archive`)
|
|
2
|
+
|
|
3
|
+
- **Estado:** aceptado
|
|
4
|
+
- **Fecha:** 2026-07-10
|
|
5
|
+
- **Decide:** lead / arquitecto de la metodología
|
|
6
|
+
|
|
7
|
+
## Contexto
|
|
8
|
+
|
|
9
|
+
OpenSpec tiene `archive`: cuando un change está completo, **funde sus delta specs** en las
|
|
10
|
+
specs canónicas (`openspec/specs/<capability>/spec.md`) y **mueve el change** a
|
|
11
|
+
`openspec/changes/archive/`. Pero en el flujo dai (`link-us → propose → apply/tdd → check
|
|
12
|
+
→ pr → dai-review → merge → stamp → done`) el `archive` **no tenía lugar**: ni cuándo, ni
|
|
13
|
+
quién. Sin eso, las specs vivas del repo quedan desincronizadas de lo shippeado.
|
|
14
|
+
|
|
15
|
+
Dos preguntas de fondo: **¿quién lo corre?** y **¿en qué momento?**
|
|
16
|
+
|
|
17
|
+
## Decisión
|
|
18
|
+
|
|
19
|
+
### `archive` = el acto de aceptar el change en las specs canónicas → lo hace **quien aprueba la PR**
|
|
20
|
+
|
|
21
|
+
Fundir un change en las specs oficiales es **bendecir** ese cambio. Por eso lo ata a la
|
|
22
|
+
**aprobación**, no a la autoría: lo corre **quien aprueba la PR**, no el autor. Es un
|
|
23
|
+
**gate de aprobación**, no un paso de cierre del autor (un autor no debería fundir sus
|
|
24
|
+
propios cambios en la spec oficial de un equipo).
|
|
25
|
+
|
|
26
|
+
Ownership por **dial de ceremonia** (N1/N2/N3):
|
|
27
|
+
|
|
28
|
+
- **N1/N2 — `dai archive` (comando):** el aprobador lo corre **en la branch de la PR** al
|
|
29
|
+
aprobar → funde los deltas + mueve el change + lo deja **sin commitear** para revisarlo →
|
|
30
|
+
lo incluye en la PR → mergea normal. **Resuelve solo el problema de base protegida**: el
|
|
31
|
+
fold viaja dentro de la PR, no hay push directo a `main`.
|
|
32
|
+
- **N3 — workflow de CI opt-in** (roadmap): el merge aprobado dispara el `archive` solo.
|
|
33
|
+
|
|
34
|
+
### `dai archive` es un **comando**, no una skill
|
|
35
|
+
|
|
36
|
+
Archivar es **mecánico** (detectar el change + `openspec archive <change> --yes`): sin
|
|
37
|
+
juicio → va al CLI, como `dai stamp`/`check`/`done` (ADR-0002). `dai-review` es skill
|
|
38
|
+
porque revisar código sí requiere juicio; archivar no. `dai archive` **envuelve**
|
|
39
|
+
`openspec archive`, detectando el change activo por su `implements.yaml` (reusa
|
|
40
|
+
`discoverImplements`); si hay varios, pide el nombre.
|
|
41
|
+
|
|
42
|
+
### `dai check` y `dai ls` saltean `openspec/changes/archive/`
|
|
43
|
+
|
|
44
|
+
Un change archivado está **shippeado**: no debe aparecer en `check` (daría un ⚠️ de drift
|
|
45
|
+
falso si el PO edita esa US después) ni en `ls`. `discoverImplements` acepta
|
|
46
|
+
`includeArchived` (default `true`); `check`/`ls` lo pasan en `false`. **`dai stamp`/`done`/
|
|
47
|
+
`archive` mantienen el default** — sí necesitan encontrar el `implements.yaml` (p. ej.
|
|
48
|
+
`stamp` post-merge, cuando el change ya se archivó en la branch).
|
|
49
|
+
|
|
50
|
+
## Consecuencias
|
|
51
|
+
|
|
52
|
+
- ✅ El `archive` queda **atado a la aprobación** — las specs canónicas reflejan solo lo bendecido.
|
|
53
|
+
- ✅ La variante `dai archive` (N1/N2) **elude la base protegida** sin tokens ni CI: el fold entra por la PR.
|
|
54
|
+
- ✅ `check`/`ls` dejan de reportar ruido de changes shippeados.
|
|
55
|
+
- ✅ Coherente con el ADN: es una **herramienta opcional** (no obliga), mecánica → CLI.
|
|
56
|
+
- ⚠️ Edge: en PRs desde **fork**, el aprobador puede no tener push a la branch del autor →
|
|
57
|
+
ahí cae al workflow de CI, o lo corre el autor.
|
|
58
|
+
- ⚠️ El **nudge** (que `/dai-review` recuerde archivar al aprobar) y el **workflow de CI**
|
|
59
|
+
quedan como follow-up (roadmap), igual que documentar el paso en `SCRUM-CON-IA`.
|
|
60
|
+
|
|
61
|
+
## Alternativas consideradas
|
|
62
|
+
|
|
63
|
+
- **Que lo corra el autor, en el PR** — descartado: fundir en la spec oficial es un acto de
|
|
64
|
+
aprobación; el autor no debería bendecir su propio cambio. Va atado a quien aprueba.
|
|
65
|
+
- **Post-merge sobre la base (commit directo)** — descartado como default: choca con branch
|
|
66
|
+
protection (push directo a `main`). Queda para el workflow de CI (con follow-up PR o App token).
|
|
67
|
+
- **Una skill `/dai-archive`** — descartado: archivar es mecánico, no necesita un LLM (ADR-0002).
|
|
68
|
+
El comando puede invocarse desde el asistente igual, sin ser skill.
|
package/docs/adr/README.md
CHANGED
|
@@ -16,6 +16,7 @@ decisión cambia, se escribe un ADR nuevo que supersede al viejo. Molde en
|
|
|
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
18
|
| [0010](0010-versionado-y-upgrade.md) | Versionado y upgrade: compatibilidad por semver, `doctor` version-drift, `dai sync` aditivo | propuesto |
|
|
19
|
+
| [0011](0011-archive-gate-de-aprobacion.md) | `archive` es un gate de aprobación: `dai archive` (comando) lo corre el aprobador; `check`/`ls` saltean `archive/` | aceptado |
|
|
19
20
|
|
|
20
21
|
> Estas son las decisiones que cierran las "Decisiones abiertas" de
|
|
21
22
|
> [`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.5.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/",
|