@dforce2055/dai 0.13.1 → 0.13.2
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 +72 -0
- package/README.md +4 -3
- package/VERSION +1 -1
- package/cli/dai.mjs +86 -5
- package/cli/lib/bootstrap.mjs +2 -1
- package/cli/lib/docs-links.mjs +23 -0
- package/cli/lib/pr.mjs +111 -25
- package/docs/EJEMPLO-END-TO-END.md +3 -1
- package/docs/guias/dev.md +5 -2
- package/package.json +2 -1
- package/templates/pull-request.md +10 -1
- package/docs/public/tutoriales/clickup-1-settings.png +0 -0
- package/docs/public/tutoriales/clickup-2-api.png +0 -0
- package/docs/public/tutoriales/clickup-3-generate-copy.png +0 -0
- package/docs/public/tutoriales/funcional-1-skills-usuario.png +0 -0
- package/docs/public/tutoriales/funcional-2-copilot-signin.png +0 -0
- package/docs/public/tutoriales/funcional-3-carpeta-configurada.png +0 -0
- package/docs/public/tutoriales/funcional-4-doctor.png +0 -0
- package/docs/public/tutoriales/funcional-5-publish-parent.png +0 -0
- package/docs/public/tutoriales/jira-1-avatar.png +0 -0
- package/docs/public/tutoriales/jira-2-seguridad-tokens.png +0 -0
- package/docs/public/tutoriales/jira-3-crear-token.png +0 -0
- package/docs/public/tutoriales/jira-4-nombre-vencimiento.png +0 -0
- package/docs/public/tutoriales/jira-5-copiar.png +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,77 @@
|
|
|
3
3
|
Formato basado en [Keep a Changelog](https://keepachangelog.com/). Versionado semver
|
|
4
4
|
(ver `VERSION`).
|
|
5
5
|
|
|
6
|
+
## [0.13.2] — 2026-09-02
|
|
7
|
+
|
|
8
|
+
**Una PR se publicaba con la descripción vacía y la lista de cambios diciendo "Cambio 1,
|
|
9
|
+
Cambio 2". No siempre: a veces salía perfecta. Lo raro es que "Enlaces relacionados", que
|
|
10
|
+
vive en el mismo template, nunca falló — y ahí estaba la pista. Y de yapa, el paquete de npm
|
|
11
|
+
adelgaza de 3.7 MB a 703 kB: cargaba las capturas del sitio.**
|
|
12
|
+
|
|
13
|
+
`dai pr` rellenaba cada sección **solo si tenía el dato** y, si no lo tenía, devolvía el
|
|
14
|
+
molde del template intacto, en silencio. La "Descripción" quedaba en su comentario HTML, que
|
|
15
|
+
no se renderiza: en GitHub y en GitLab la sección se ve **vacía**. El bloque de enlaces nunca
|
|
16
|
+
falló porque su código siempre escribe — si no encuentra la sección, la agrega. Esa
|
|
17
|
+
disciplina ahora vale para todo el cuerpo de la PR.
|
|
18
|
+
|
|
19
|
+
Detrás del bug había algo más de fondo: **no existía forma de escribir la descripción**.
|
|
20
|
+
`dai pr` no aceptaba ningún texto, así que el propósito de la PR solo podía salir del título
|
|
21
|
+
de la US y de los subjects de los commits. Ni el dev ni su agente podían hacerlo bien aunque
|
|
22
|
+
quisieran.
|
|
23
|
+
|
|
24
|
+
### Arreglado
|
|
25
|
+
- **La PR salía con el molde del template sin llenar.** Tres caminos llevaban al mismo
|
|
26
|
+
resultado, los tres silenciosos: (1) el tracker no contestaba —sin token, sin red, `DAI_PM`
|
|
27
|
+
mal seteado— y sin el título de la US no se llenaba "Descripción"; (2) la branch base no
|
|
28
|
+
existía **en local** —clones `--single-branch`, repos donde se trabaja sobre `develop` y la
|
|
29
|
+
base es `main`— y `git log base..HEAD` fallaba dentro de un `catch` vacío, dejando "Cambios
|
|
30
|
+
realizados" con `Cambio 1 / Cambio 2`; (3) una branch exenta (`chore/`, `docs/`) nunca
|
|
31
|
+
llenaba "Descripción", ni siquiera pudiendo. Ese mismo `catch` vacío también se comía el
|
|
32
|
+
chequeo de *"sin commits sobre la base no hay PR"*.
|
|
33
|
+
- **La branch base ahora se resuelve** a `main` o, si no está en local, a `origin/main`.
|
|
34
|
+
- **Las secciones se reconocen aunque el repo tenga su propio molde** (`## 📝 Descripción del
|
|
35
|
+
cambio`, `### CAMBIOS REALIZADOS`): antes el match era exacto, no encontraba la sección y
|
|
36
|
+
devolvía el body sin tocar — otra vez, sin decir nada. Ignora los `##` que estén dentro de
|
|
37
|
+
un bloque de código y, si la sección no existe, la agrega.
|
|
38
|
+
|
|
39
|
+
### Agregado
|
|
40
|
+
- **`dai pr --description "…"` y `--description-file <archivo.md>`** — el propósito de un
|
|
41
|
+
cambio no se deriva de git ni del tracker. dai llena la US, el estado del check, los commits
|
|
42
|
+
y los links; el porqué lo escribe quien crea la PR. **dai no lo inventa: lo pide.**
|
|
43
|
+
- **`--changes` y `--changes-file`** — reemplazan el detalle de "Cambios realizados" cuando los
|
|
44
|
+
commits no cuentan bien la historia. Sin ellos, siguen saliendo de los commits de la branch.
|
|
45
|
+
- **La constitución que escriben `dai init` y `dai sync` lo dice**, para que el agente que
|
|
46
|
+
corre `dai pr` sepa que la descripción es suya. Los repos ya inicializados la reciben con
|
|
47
|
+
`dai sync`.
|
|
48
|
+
|
|
49
|
+
### Cambiado
|
|
50
|
+
- ⚠️ **`dai pr` no publica una PR que saldría con el molde sin llenar.** Con `--yes` o sin TTY
|
|
51
|
+
—el camino de un agente o de CI— **aborta** y dice qué flag pasar; en terminal avisa y decide
|
|
52
|
+
la persona. **Si tenés automatización con `dai pr --yes` sin `--description`, se va a frenar**:
|
|
53
|
+
es justamente lo que publicaba las PRs vacías. El molde también se detecta en la cabecera
|
|
54
|
+
(`ABC-###`), la misma familia de los issues #31/#33.
|
|
55
|
+
|
|
56
|
+
### Arreglado — el paquete de npm
|
|
57
|
+
- **`npm i -g @dforce2055/dai` bajaba las capturas del sitio.** `files[]` lista `docs`
|
|
58
|
+
entero, y ahí adentro viven las de los tutoriales (`docs/public/tutoriales/*.png`): **2.9 MB
|
|
59
|
+
de los 3.7 MB del paquete**, que el CLI no abre nunca. El test de higiene que ya cubría el
|
|
60
|
+
sitio (`index.html`, `onboarding.html`) no las veía porque entraban por otra puerta.
|
|
61
|
+
**3.7 MB → 703 kB**, 130 → 117 archivos, cero PNG en el tarball ([#37](https://github.com/dforce2055/dai/issues/37)).
|
|
62
|
+
- **`dai docs` copiaba imágenes que no se veían.** Los `.md` referencian las capturas con la
|
|
63
|
+
ruta absoluta del sitio (``, que VitePress resuelve contra
|
|
64
|
+
`docs/public/`); fuera del sitio esa ruta apunta a la raíz del filesystem, así que en la
|
|
65
|
+
copia **ya estaban rotas**, con los 2.9 MB adentro y todo. Ahora `dai docs` no copia
|
|
66
|
+
`public/` —son assets del sitio, no documentación para leer desde un repo— y **absolutiza**
|
|
67
|
+
esos links contra el sitio publicado: la doc copiada por fin muestra las capturas.
|
|
68
|
+
|
|
69
|
+
### Interno
|
|
70
|
+
- **358 tests** (+19). Uno por cada camino que dejaba pasar el molde del template —tracker
|
|
71
|
+
caído, sin commits, branch sin US, template propio del repo, headings dentro de un fence, y
|
|
72
|
+
el caso peor (sin tracker y sin commits a la vez)— más los de la reescritura de links y dos
|
|
73
|
+
de higiene que fijan la regla del paquete: que `files[]` declare la exclusión, y que no
|
|
74
|
+
aparezcan imágenes versionadas bajo `docs/` fuera de esa carpeta. Si mañana una captura
|
|
75
|
+
aterriza en otro lado, el test obliga a decidir ahí, no midiendo el tarball.
|
|
76
|
+
|
|
6
77
|
## [0.13.1] — 2026-08-24
|
|
7
78
|
|
|
8
79
|
**Un dev en Windows no podía pushear contra el GitLab de su empresa. `ssh -T` le autenticaba
|
|
@@ -737,6 +808,7 @@ ClickUp y Jira Cloud.
|
|
|
737
808
|
- Tests de las rutas de red (jira/clickup/forge) con `fetch` mockeado. Sin links rotos;
|
|
738
809
|
`files` de npm sin tests ni secretos.
|
|
739
810
|
|
|
811
|
+
[0.13.2]: https://github.com/dforce2055/dai/releases/tag/v0.13.2
|
|
740
812
|
[0.13.1]: https://github.com/dforce2055/dai/releases/tag/v0.13.1
|
|
741
813
|
[0.13.0]: https://github.com/dforce2055/dai/releases/tag/v0.13.0
|
|
742
814
|
[0.12.0]: https://github.com/dforce2055/dai/releases/tag/v0.12.0
|
package/README.md
CHANGED
|
@@ -103,7 +103,8 @@ dai link-us <ID> # trae la US del tracker → branch + implements
|
|
|
103
103
|
```bash
|
|
104
104
|
dai check # ¿tu código sigue al día con la US? ✅ / ⚠️ atrasado
|
|
105
105
|
# revisas tu propio código + smoke test local antes de la PR
|
|
106
|
-
dai pr --assignee <compañero>
|
|
106
|
+
dai pr --assignee <compañero> \
|
|
107
|
+
--description "Qué resuelve y por qué" # crea la PR precargada y se la asigna a un compañero
|
|
107
108
|
```
|
|
108
109
|
```text
|
|
109
110
|
/dai-review <PR> # tu compañero deja un review inline (comentario por línea); un humano aprueba
|
|
@@ -201,14 +202,14 @@ flowchart TD
|
|
|
201
202
|
| `dai link-us <ID> --resync` | re-estampa el `ac_hash` contra la US viva (tras un ⚠️ de check) |
|
|
202
203
|
| `dai check` | compara tu código vs la US viva → ✅ al día / ⚠️ atrasado (exit code = gate de PR) |
|
|
203
204
|
| `dai ls [--json]` | lista las US que implementa el repo + su link al tracker |
|
|
204
|
-
| `dai pr [--assignee u] [--base b] [--draft] [--yes] [--us ID] [--title t]` · alias **`dai mr`** | crea TU PR/MR precargada: pregunta la branch base (default `main`), muestra el texto y confirma antes de publicar. Detecta el forge (GitHub→PR con `gh` · GitLab→MR con `glab`); `mr` es el mismo comando, más natural en GitLab. **La US la resuelve la branch** (la nombra `dai link-us`): si hay varias vivas y ninguna coincide, **pregunta** en vez de elegir por vos (sin TTY falla pidiendo `--us <ID>`), y una branch `chore/`/`docs/` sale **sin US** en lugar de heredar la de otro |
|
|
205
|
+
| `dai pr [--assignee u] [--base b] [--draft] [--yes] [--us ID] [--title t] [--description t\|--description-file f] [--changes t\|--changes-file f]` · alias **`dai mr`** | crea TU PR/MR precargada: pregunta la branch base (default `main`), muestra el texto y confirma antes de publicar. Detecta el forge (GitHub→PR con `gh` · GitLab→MR con `glab`); `mr` es el mismo comando, más natural en GitLab. **La US la resuelve la branch** (la nombra `dai link-us`): si hay varias vivas y ninguna coincide, **pregunta** en vez de elegir por vos (sin TTY falla pidiendo `--us <ID>`), y una branch `chore/`/`docs/` sale **sin US** en lugar de heredar la de otro. **La descripción la escribís vos (o tu agente) con `--description`**: dai llena la US, los commits y los links, pero no inventa el propósito de un cambio — si "Descripción" o "Cambios realizados" quedarían con el molde del template, con `--yes` o sin TTY **no publica** y te dice qué falta |
|
|
205
206
|
| `dai stamp` | estampa la cobertura inversa en el tracker (branch + commit-ancla) |
|
|
206
207
|
| `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 |
|
|
207
208
|
| `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` |
|
|
208
209
|
| `dai forge review <ref> --from <review.json>` `[--dry-run\|--yes]` | **review inline**: un resumen + un comentario anclado a cada `archivo:línea`, clasificado low/medium/high. **Valida cada posición contra el diff** (descarta lo que el modelo inventó) antes de postear; sin `--yes` muestra el preview y no postea nada. Modo desatendido: `--min-severity`/`--min-confidence`/`--max-comments`. El review sale con `event: COMMENT`, nunca `APPROVE` ([ADR-0016](docs/adr/0016-review-inline.md)) |
|
|
209
210
|
| `dai forge comment <ref> --body-file <f>` · `dai forge pr <ref>` | comentar / leer una PR/MR (GitHub/GitLab) — el fallback simple, sin anclar |
|
|
210
211
|
| `dai ac-hash <us.md>` | calcula el hash de los criterios de aceptación de una US |
|
|
211
|
-
| `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) |
|
|
212
|
+
| `dai doctor` · `dai docs <dest>` · `dai version` | diagnóstico del entorno (incluye **version-drift** del scaffold) · copiar la doc (sin los assets del sitio; los links a las capturas apuntan al sitio publicado) · versión (`dai version` avisa si tu repo quedó atrás) |
|
|
212
213
|
|
|
213
214
|
> **🆕 Mantené tu repo al día — `dai sync`.** Las skills, la constitución y los templates son un
|
|
214
215
|
> *caché derivable* del CLI. Cuando actualizás `dai` (`dai upgrade`), **`dai doctor` y
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.13.
|
|
1
|
+
0.13.2
|
package/cli/dai.mjs
CHANGED
|
@@ -27,7 +27,8 @@ import { branchUrl, commitUrl, parseRemote, detectForge } from "./lib/forge-url.
|
|
|
27
27
|
import { parsePrRef, getPR, postComment, postReview } from "./lib/forge-api.mjs";
|
|
28
28
|
import { trackerUrl } from "./lib/tracker-url.mjs";
|
|
29
29
|
import { parseFindings, diffPositions, validateFindings, filterFindings, renderFindingBody, renderReviewSummary } from "./lib/review-findings.mjs";
|
|
30
|
-
import { composePrBody, prTitle, forgeTool } from "./lib/pr.mjs";
|
|
30
|
+
import { composePrBody, prTitle, forgeTool, bodyGaps } from "./lib/pr.mjs";
|
|
31
|
+
import { absolutizeSiteLinks } from "./lib/docs-links.mjs";
|
|
31
32
|
import { diagnoseGitSsh, pushFailureHint, WINDOWS_OPENSSH } from "./lib/git-ssh.mjs";
|
|
32
33
|
import { dirsEqual } from "./lib/fsutil.mjs";
|
|
33
34
|
import { parseFlags, parseAssistants, isAssistantToken, asList } from "./lib/args.mjs";
|
|
@@ -216,6 +217,27 @@ function gitUser() {
|
|
|
216
217
|
function gitRemote() { try { return git(["remote", "get-url", "origin"]); } catch { return null; } }
|
|
217
218
|
function gitBranch() { try { return git(["rev-parse", "--abbrev-ref", "HEAD"]); } catch { return null; } }
|
|
218
219
|
function gitCommit() { try { return git(["rev-parse", "HEAD"]); } catch { return null; } }
|
|
220
|
+
// ¿Existe esta ref como commit? Devuelve la ref si sí, null si no. Sirve para caer de
|
|
221
|
+
// `main` a `origin/main` sin adivinar: en muchos repos la base solo existe en el remoto.
|
|
222
|
+
function resolveRev(ref) {
|
|
223
|
+
try { git(["rev-parse", "--verify", "--quiet", `${ref}^{commit}`]); return ref; } catch { return null; }
|
|
224
|
+
}
|
|
225
|
+
// Un texto que se puede pasar inline (`--description "…"`) o por archivo
|
|
226
|
+
// (`--description-file notas.md`). Un agente casi siempre quiere el archivo: markdown
|
|
227
|
+
// multilínea no sobrevive entero a la línea de comandos.
|
|
228
|
+
function textOpt(opts, name) {
|
|
229
|
+
const file = opts[`${name}File`];
|
|
230
|
+
if (file !== undefined) {
|
|
231
|
+
if (file === true) fail(`--${name}-file necesita la ruta de un archivo.`, 1);
|
|
232
|
+
const path = Array.isArray(file) ? file[file.length - 1] : file;
|
|
233
|
+
try { return readFileSync(path, "utf8"); }
|
|
234
|
+
catch { fail(`no pude leer --${name}-file '${path}'.`, 1); }
|
|
235
|
+
}
|
|
236
|
+
const v = opts[name];
|
|
237
|
+
if (v === true) fail(`--${name} necesita un texto (o usá --${name}-file <archivo>).`, 1);
|
|
238
|
+
if (Array.isArray(v)) return v.join("\n");
|
|
239
|
+
return typeof v === "string" ? v : null;
|
|
240
|
+
}
|
|
219
241
|
|
|
220
242
|
// ── check --ci: el gate de governance/ci-rules.md, ejecutable ────────────────
|
|
221
243
|
//
|
|
@@ -879,9 +901,16 @@ async function cmdPr(opts) {
|
|
|
879
901
|
let base = opts.base;
|
|
880
902
|
if (!base) { const ans = await ask(" ¿Contra qué branch va la PR? (main) "); base = ans || "main"; }
|
|
881
903
|
|
|
904
|
+
// La base puede no existir LOCAL (clones con --single-branch, repos donde el dev
|
|
905
|
+
// trabaja sobre develop y la base es main, corporativos con la base solo en origin).
|
|
906
|
+
// Antes el catch vacío se comía el error y seguía de largo: ni se contaban los commits
|
|
907
|
+
// (→ "Cambios realizados" salía con el molde) ni frenaba el chequeo de abajo.
|
|
908
|
+
const baseRev = resolveRev(base) || resolveRev(`origin/${base}`);
|
|
909
|
+
if (!baseRev) warn(`no encuentro la branch base '${base}' (ni local ni en origin/${base}). Trae la base primero: git fetch origin ${base}`);
|
|
910
|
+
|
|
882
911
|
// Sin commits sobre la base no hay PR.
|
|
883
912
|
let ahead = null;
|
|
884
|
-
try { ahead = Number(git(["rev-list", "--count", `${
|
|
913
|
+
try { ahead = Number(git(["rev-list", "--count", `${baseRev}..HEAD`])); } catch { /* base ausente */ }
|
|
885
914
|
if (ahead === 0) {
|
|
886
915
|
fail(`no hay commits en '${branch}' por encima de '${base}'. Una PR necesita cambios: haz commit primero (git commit).`, 1);
|
|
887
916
|
}
|
|
@@ -944,7 +973,13 @@ async function cmdPr(opts) {
|
|
|
944
973
|
: join(ROOT, "templates", "pull-request.md");
|
|
945
974
|
// Commits de la branch (para precargar "Cambios realizados").
|
|
946
975
|
let commits = [];
|
|
947
|
-
|
|
976
|
+
if (baseRev) {
|
|
977
|
+
try { commits = git(["log", `${baseRev}..HEAD`, "--pretty=%s"]).split("\n").filter(Boolean); } catch { /* rango inválido */ }
|
|
978
|
+
}
|
|
979
|
+
// El texto que escribe quien crea la PR (el dev o su agente). Es la ÚNICA fuente de
|
|
980
|
+
// una descripción de verdad: dai lee git y el tracker, no el porqué del cambio.
|
|
981
|
+
const description = textOpt(opts, "description");
|
|
982
|
+
const changes = textOpt(opts, "changes");
|
|
948
983
|
// La canónica del tracker (live.url) gana sobre la derivada; el template gana sobre todo.
|
|
949
984
|
const usUrl = id ? usUrlFor(id, live?.url) : null;
|
|
950
985
|
if (id && !usUrl) {
|
|
@@ -952,7 +987,8 @@ async function cmdPr(opts) {
|
|
|
952
987
|
warn(`configurá DAI_TRACKER_URL_TEMPLATE en el .env.dai (p. ej. https://tu-tracker/browse/{id}).`);
|
|
953
988
|
}
|
|
954
989
|
const body = composePrBody(readFileSync(tplPath, "utf8"), {
|
|
955
|
-
id, version, ac_hash, status, usUrl, usTitle: live?.title, commits,
|
|
990
|
+
id, version, ac_hash, status, usUrl, usTitle: live?.title, commits, description, changes,
|
|
991
|
+
noUsReason: id ? null : scope.reason,
|
|
956
992
|
branch, branchUrl: branchUrl(remote, branch), commit, commitUrl: commitUrl(remote, commit),
|
|
957
993
|
});
|
|
958
994
|
// Sin US el título sale del último commit: describe lo que hay adentro, en vez de
|
|
@@ -969,6 +1005,27 @@ async function cmdPr(opts) {
|
|
|
969
1005
|
process.stdout.write(` ─────────────────────────────────────────────────────\n\n${body}\n`);
|
|
970
1006
|
process.stdout.write(` ─────────────────────────────────────────────────────\n`);
|
|
971
1007
|
|
|
1008
|
+
// 4b. Gate: una PR con el molde del template sin llenar no se puede revisar.
|
|
1009
|
+
// Pasaba en repos reales — "Descripción" con el comentario HTML (que no se renderiza:
|
|
1010
|
+
// la sección se ve VACÍA) y "Cambios realizados" con `Cambio 1/Cambio 2` — cada vez
|
|
1011
|
+
// que el tracker no respondía o la base no estaba local. dai NO inventa la descripción:
|
|
1012
|
+
// la pide. Sin TTY (el camino del agente) frena; en terminal avisa y decidís vos.
|
|
1013
|
+
const gaps = bodyGaps(body);
|
|
1014
|
+
if (gaps.length) {
|
|
1015
|
+
const how =
|
|
1016
|
+
` Escribí el texto y pasáselo a dai:\n` +
|
|
1017
|
+
` dai pr --description "Qué resuelve este cambio y por qué (2–4 líneas)."\n` +
|
|
1018
|
+
` dai pr --description-file notas.md --changes-file cambios.md (markdown multilínea)\n` +
|
|
1019
|
+
` El detalle de "Cambios realizados" sale de los commits si no pasás --changes.\n`;
|
|
1020
|
+
if (opts.yes || !process.stdin.isTTY) {
|
|
1021
|
+
closeRl();
|
|
1022
|
+
fail(`la PR saldría con el molde del template sin llenar: ${gaps.join(", ")}.\n` +
|
|
1023
|
+
` Una PR sin descripción no se puede revisar, así que no la publico.\n${how}`, 1);
|
|
1024
|
+
}
|
|
1025
|
+
warn(`la PR va a salir con el molde sin llenar: ${gaps.join(", ")}.`);
|
|
1026
|
+
process.stdout.write(how);
|
|
1027
|
+
}
|
|
1028
|
+
|
|
972
1029
|
// Archivo de paso para gh/glab: en el temp del sistema, NO en el repo (no lo ensucia).
|
|
973
1030
|
const bodyFile = join(mkdtempSync(join(tmpdir(), "dai-pr-")), "body.md");
|
|
974
1031
|
if (!opts.yes) {
|
|
@@ -1431,8 +1488,28 @@ async function cmdInit(repo, opts) {
|
|
|
1431
1488
|
function cmdDocs(dest) {
|
|
1432
1489
|
if (!dest) fail("uso: dai docs <destino>");
|
|
1433
1490
|
mkdirSync(dest, { recursive: true });
|
|
1434
|
-
|
|
1491
|
+
// `public/` son los assets del sitio (VitePress), no documentación para leer desde el
|
|
1492
|
+
// repo de nadie: las capturas de los tutoriales ni siquiera viajan en el paquete npm
|
|
1493
|
+
// (issue #37). Se saltea, y los links que las nombran se absolutizan contra el sitio.
|
|
1494
|
+
cpSync(join(ROOT, "docs"), dest, { recursive: true, filter: (src) => !/[/\\]public([/\\]|$)/.test(src) });
|
|
1495
|
+
let reescritos = 0;
|
|
1496
|
+
for (const f of walkMd(dest)) {
|
|
1497
|
+
const md = readFileSync(f, "utf8");
|
|
1498
|
+
const out = absolutizeSiteLinks(md);
|
|
1499
|
+
if (out !== md) { writeFileSync(f, out); reescritos++; }
|
|
1500
|
+
}
|
|
1435
1501
|
ok(`documentación copiada a ${dest}`);
|
|
1502
|
+
if (reescritos) info(`${reescritos} documento(s) con capturas: los links apuntan al sitio publicado.`);
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
// Los .md de un árbol, para la reescritura de links de cmdDocs.
|
|
1506
|
+
function walkMd(dir, out = []) {
|
|
1507
|
+
for (const name of readdirSync(dir)) {
|
|
1508
|
+
const p = join(dir, name);
|
|
1509
|
+
if (statSync(p).isDirectory()) walkMd(p, out);
|
|
1510
|
+
else if (name.endsWith(".md")) out.push(p);
|
|
1511
|
+
}
|
|
1512
|
+
return out;
|
|
1436
1513
|
}
|
|
1437
1514
|
|
|
1438
1515
|
// ── archive: funde los delta specs del change en los specs canónicos y lo archiva ─
|
|
@@ -1845,6 +1922,10 @@ switch (cmd) {
|
|
|
1845
1922
|
" 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" +
|
|
1846
1923
|
" pr (alias mr) [--assignee u] [--base b] [--draft] [--yes] crea TU PR/MR precargada (muestra + confirma)\n" +
|
|
1847
1924
|
" [--us <ID>] [--title t] la US la resuelve la branch; si hay varias, pregunta (sin TTY, falla)\n" +
|
|
1925
|
+
" --description <texto> QUÉ resuelve la PR y por qué → sección 'Descripción' (o --description-file <f>)\n" +
|
|
1926
|
+
" --changes <texto> detalle de 'Cambios realizados' (default: los commits) (o --changes-file <f>)\n" +
|
|
1927
|
+
" sin descripción y sin commits, con --yes o sin TTY, dai NO publica: la PR\n" +
|
|
1928
|
+
" saldría con el molde del template y no se podría revisar\n" +
|
|
1848
1929
|
" forge comment <ref> --body-file <f> · forge pr <ref> comentar/leer una PR ajena (github/gitlab)\n" +
|
|
1849
1930
|
" forge review <ref> --from <review.json> [--dry-run|--yes] review inline: resumen + comentario por línea\n" +
|
|
1850
1931
|
" --min-severity low|medium|high · --min-confidence 0..1 · --max-comments N · --base <branch>\n" +
|
package/cli/lib/bootstrap.mjs
CHANGED
|
@@ -277,6 +277,7 @@ export function constitution(kind) {
|
|
|
277
277
|
- **La IA confirma antes de construir:** el asistente declara que entendió esta constitución y la va a obedecer antes de generar código.
|
|
278
278
|
- **Secretos:** en \`.env.dai\` (NO versionado; el \`.env\` del equipo no se toca). git por **SSH**, APIs por **token scopeado**.
|
|
279
279
|
- **No bajes la seguridad para avanzar:** si una llamada falla por el certificado, declara la CA (\`NODE_EXTRA_CA_CERTS\`). **Nunca** \`NODE_TLS_REJECT_UNAUTHORIZED=0\`, \`verify=False\`, \`-k\` ni equivalentes: apagan la verificación de toda la conexión, y por ahí viajan los tokens.
|
|
280
|
+
- **La PR la escribe quien la crea:** \`dai pr\` precarga la US, el estado del check, los commits y los links, pero **la Descripción la escribes tú**: \`dai pr --description "qué resuelve y por qué"\` (o \`--description-file <archivo.md>\` para markdown multilínea, y \`--changes\` si los commits no cuentan bien la historia). dai **no inventa** el propósito de un cambio: si la Descripción o los Cambios realizados quedarían con el molde del template, no publica la PR. Una PR sin descripción no se puede revisar.
|
|
280
281
|
- **Si el CLI no llega, para y dilo:** cuando \`dai\` no cubre un caso, repórtalo — no improvises una llamada a la API por fuera. El atajo publica igual, pero rompe el link QUÉ↔CÓMO en silencio y nadie se entera hasta que la trazabilidad ya está mal.
|
|
281
282
|
- **Docs vivas:** una constitución o arquitectura desactualizada es un defecto, no documentación.
|
|
282
283
|
- Separa el QUÉ (funcional) del CÓMO (técnico); no mezcles.
|
|
@@ -293,7 +294,7 @@ export function constitution(kind) {
|
|
|
293
294
|
|
|
294
295
|
- **Skills (el QUÉ):** \`doc-to-backlog\` (un doc → backlog) · \`grill-intent\` (Gate 0) · \`grill-epic\` (épicas) · \`grill-user-story\` (la US)
|
|
295
296
|
- **Skills (el CÓMO):** \`link-us\`, \`tdd\`, \`dai-review\`
|
|
296
|
-
- **CLI:** \`dai link-us <ID>\` · \`dai check\` · \`dai stamp\` · \`dai pr\` · \`dai ls\`
|
|
297
|
+
- **CLI:** \`dai link-us <ID>\` · \`dai check\` · \`dai stamp\` · \`dai pr --description "…"\` · \`dai ls\`
|
|
297
298
|
- **Formatos:** \`.dai/templates/\` · **Governance:** \`.dai/governance/\`
|
|
298
299
|
|
|
299
300
|
Detalle completo de la metodología: ${REPO_URL}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// dai · reescribir los links absolutos del sitio cuando la doc se copia afuera.
|
|
2
|
+
//
|
|
3
|
+
// Los .md de docs/ son la fuente del sitio (VitePress, `base: "/dai/docs/"`), así que las
|
|
4
|
+
// capturas se referencian con la ruta absoluta del sitio: ``, que
|
|
5
|
+
// VitePress resuelve contra docs/public/. Fuera del sitio esa ruta no resuelve a nada: en
|
|
6
|
+
// una copia hecha con `dai docs`, `/tutoriales/x.png` apunta a la raíz del filesystem (o
|
|
7
|
+
// del repo, si lo renderiza GitHub). O sea que la imagen ya estaba rota en la copia,
|
|
8
|
+
// tuviera o no el paquete los 2.9 MB de PNG adentro (issue #37).
|
|
9
|
+
//
|
|
10
|
+
// Al copiar, esos links se vuelven absolutos contra el sitio publicado: la doc copiada
|
|
11
|
+
// muestra las capturas, y el paquete de npm no las carga.
|
|
12
|
+
|
|
13
|
+
export const SITE_DOCS = "https://dforce2055.github.io/dai/docs";
|
|
14
|
+
|
|
15
|
+
// Rutas absolutas del sitio que aparecen en los .md. Se listan a propósito en vez de
|
|
16
|
+
// reescribir toda `](/…)`: un link a `/etc/hosts` en un ejemplo no es un link del sitio.
|
|
17
|
+
const SITE_PATHS = /\]\(\/(tutoriales\/[^)\s]+)\)/g;
|
|
18
|
+
|
|
19
|
+
// Devuelve el markdown con los links del sitio apuntando al sitio publicado.
|
|
20
|
+
// Idempotente: una URL ya absoluta no matchea (el patrón exige `](/`).
|
|
21
|
+
export function absolutizeSiteLinks(md) {
|
|
22
|
+
return String(md).replace(SITE_PATHS, (_, path) => `](${SITE_DOCS}/${path})`);
|
|
23
|
+
}
|
package/cli/lib/pr.mjs
CHANGED
|
@@ -4,48 +4,134 @@
|
|
|
4
4
|
|
|
5
5
|
const EMOJI = { "al-dia": "✅ al día", atrasado: "⚠️ atrasado", "sin-us": "❓ sin US" };
|
|
6
6
|
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
7
|
+
// Las secciones que dai se compromete a entregar llenas. Si alguna sale con el molde
|
|
8
|
+
// del template, la PR se publica vacía y el review no tiene qué mirar (era el bug:
|
|
9
|
+
// "Descripción" con el comentario HTML y "Cambios realizados" con `Cambio 1/Cambio 2`).
|
|
10
|
+
// `dai pr` las verifica con bodyGaps() antes de publicar.
|
|
11
|
+
export const OWNED_SECTIONS = ["Descripción", "Cambios realizados"];
|
|
12
|
+
|
|
13
|
+
// ── Secciones ────────────────────────────────────────────────────────────────
|
|
14
|
+
// Normaliza un heading para compararlo: sin acentos, sin emoji ni puntuación, minúscula.
|
|
15
|
+
// Un repo puede traer su propio template ("## 📝 Descripción del cambio") y dai igual
|
|
16
|
+
// tiene que reconocer la sección: con el match exacto de antes no la encontraba y
|
|
17
|
+
// devolvía el body intacto EN SILENCIO, que es la forma más cara de fallar.
|
|
18
|
+
const norm = (s) => String(s).normalize("NFD").replace(/[\u0300-\u036f]/gu, "")
|
|
19
|
+
.replace(/[^\p{L}\p{N}]+/gu, " ").trim().toLowerCase();
|
|
20
|
+
|
|
21
|
+
// Ubica una sección markdown (## Heading … hasta el próximo heading de igual o menor
|
|
22
|
+
// nivel). Ignora lo que esté dentro de un bloque de código: un `## ` en un fence es
|
|
23
|
+
// texto, no estructura. Devuelve null si no está.
|
|
24
|
+
function findSection(lines, heading) {
|
|
25
|
+
const want = norm(heading);
|
|
26
|
+
let start = -1, level = 0, fence = false;
|
|
27
|
+
for (let i = 0; i < lines.length; i++) {
|
|
28
|
+
if (/^\s*(```|~~~)/.test(lines[i])) { fence = !fence; continue; }
|
|
29
|
+
if (fence) continue;
|
|
30
|
+
const m = /^(#{2,6})[ \t]+(.+?)[ \t]*$/.exec(lines[i]);
|
|
31
|
+
if (!m) continue;
|
|
32
|
+
if (start === -1) { if (norm(m[2]).startsWith(want)) { start = i; level = m[1].length; } }
|
|
33
|
+
else if (m[1].length <= level) return { start, end: i, level };
|
|
34
|
+
}
|
|
35
|
+
return start === -1 ? null : { start, end: lines.length, level };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// El cuerpo crudo de una sección, o null si la sección no existe.
|
|
39
|
+
export function sectionBody(body, heading) {
|
|
40
|
+
const lines = body.split("\n");
|
|
41
|
+
const at = findSection(lines, heading);
|
|
42
|
+
return at ? lines.slice(at.start + 1, at.end).join("\n") : null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Reemplaza el cuerpo de una sección por content. Tolerante: si no encuentra la
|
|
46
|
+
// sección, devuelve el body igual (para eso está upsertSection).
|
|
10
47
|
export function replaceSection(body, heading, content) {
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
if (!
|
|
14
|
-
return
|
|
48
|
+
const lines = body.split("\n");
|
|
49
|
+
const at = findSection(lines, heading);
|
|
50
|
+
if (!at) return body;
|
|
51
|
+
return [...lines.slice(0, at.start + 1), "", content, "", ...lines.slice(at.end)].join("\n");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Como replaceSection, pero si la sección no está la agrega al final con su heading.
|
|
55
|
+
// Misma disciplina que el bloque de enlaces: dai siempre entrega el dato, nunca lo
|
|
56
|
+
// pierde porque el template del repo no tenía dónde ponerlo. content null = no sé
|
|
57
|
+
// nada → no toco (y bodyGaps lo va a reportar; dai no inventa contenido).
|
|
58
|
+
export function upsertSection(body, heading, content) {
|
|
59
|
+
if (content == null || !String(content).trim()) return body;
|
|
60
|
+
if (findSection(body.split("\n"), heading)) return replaceSection(body, heading, content);
|
|
61
|
+
return `${body.replace(/\s*$/, "")}\n\n## ${heading}\n\n${content}\n`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ── Detección del molde sin llenar ───────────────────────────────────────────
|
|
65
|
+
// Moldes conocidos del template: líneas que no dicen nada sobre ESTE cambio.
|
|
66
|
+
const MOCK_LINES = [/^-?\s*\[[ x]\]\s*cambio\s*\d+\s*$/i, /^cambio\s*\d+\s*$/i, /^-?\s*\[[ x]\]\s*$/];
|
|
67
|
+
|
|
68
|
+
// ¿El cuerpo de una sección es puro molde? Los comentarios HTML no cuentan como
|
|
69
|
+
// contenido: no se renderizan, así que en la PR publicada la sección se ve VACÍA
|
|
70
|
+
// (por eso el bug pasaba desapercibido hasta que alguien abría la PR).
|
|
71
|
+
export function isPlaceholder(text) {
|
|
72
|
+
const lines = String(text ?? "").replace(/<!--[\s\S]*?-->/g, "").split("\n").map((l) => l.trim()).filter(Boolean);
|
|
73
|
+
if (!lines.length) return true;
|
|
74
|
+
return lines.every((l) => MOCK_LINES.some((re) => re.test(l)));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Qué le falta al body para ser revisable. Vacío = la PR se puede publicar.
|
|
78
|
+
// `dai pr` lo usa como gate: sin TTY (el camino del agente) falla en vez de publicar
|
|
79
|
+
// una PR que nadie puede revisar.
|
|
80
|
+
export function bodyGaps(body) {
|
|
81
|
+
const gaps = OWNED_SECTIONS.filter((h) => {
|
|
82
|
+
const s = sectionBody(body, h);
|
|
83
|
+
return s === null || isPlaceholder(s);
|
|
84
|
+
});
|
|
85
|
+
// Placeholders de la cabecera: la PR saldría diciendo `ABC-###` (issues #31/#33).
|
|
86
|
+
if (/ABC-###|`<hash>`|@ `vX`/.test(body)) gaps.push("🔗 Implementa");
|
|
87
|
+
return gaps;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ── Composición ──────────────────────────────────────────────────────────────
|
|
91
|
+
// Descripción: lo que escribió quien crea la PR (agente o dev) gana; si no, se deriva
|
|
92
|
+
// de la US. Si dai no sabe nada, devuelve null: no inventa el propósito de un cambio.
|
|
93
|
+
export function descriptionFor(d) {
|
|
94
|
+
const explicit = String(d.description ?? "").trim();
|
|
95
|
+
if (explicit) return explicit;
|
|
96
|
+
if (d.usTitle) return `Implementa la US **${d.usTitle}** (\`${d.id}\`). Ver los criterios de aceptación en el tracker.`;
|
|
97
|
+
return null;
|
|
15
98
|
}
|
|
16
99
|
|
|
17
|
-
//
|
|
18
|
-
//
|
|
100
|
+
// Cambios realizados: el detalle explícito gana; si no, los commits de la branch
|
|
101
|
+
// (como `gh pr create --fill`). Sin ninguno de los dos, null.
|
|
102
|
+
export function changesFor(d) {
|
|
103
|
+
const explicit = String(d.changes ?? "").trim();
|
|
104
|
+
if (explicit) return explicit;
|
|
105
|
+
if (d.commits && d.commits.length) return d.commits.map((c) => `- [x] ${c}`).join("\n");
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Rellena el template del PR con los datos precargados. Tolerante con el resto del
|
|
110
|
+
// template (checklists, secciones propias del repo): dai suma, no borra.
|
|
19
111
|
export function composePrBody(template, d) {
|
|
112
|
+
let b = d.id ? fillUsHeader(template, d) : fillNoUsHeader(template, d);
|
|
113
|
+
b = upsertSection(b, "Descripción", descriptionFor(d));
|
|
114
|
+
b = upsertSection(b, "Cambios realizados", changesFor(d));
|
|
115
|
+
return upsertLinksBlock(b, d);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function fillUsHeader(template, d) {
|
|
20
119
|
let b = template;
|
|
21
|
-
if (!d.id) return composePrBodyNoUs(b, d);
|
|
22
120
|
b = b.replace(/`ABC-###`/g, `\`${d.id}\``);
|
|
23
121
|
b = b.replace(/@ `vX`/g, `@ \`${d.version}\``);
|
|
24
122
|
b = b.replace(/`<hash>`/g, `\`${d.ac_hash}\``);
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
// Descripción: default desde la US (el humano lo pule).
|
|
28
|
-
if (d.usTitle) b = replaceSection(b, "Descripción", `Implementa la US **${d.usTitle}** (\`${d.id}\`). Ver los criterios de aceptación en el tracker.`);
|
|
29
|
-
// Cambios realizados: de los commits de la branch (como gh pr create --fill).
|
|
30
|
-
if (d.commits && d.commits.length) {
|
|
31
|
-
b = replaceSection(b, "Cambios realizados", d.commits.map((c) => `- [x] ${c}`).join("\n"));
|
|
32
|
-
}
|
|
33
|
-
return upsertLinksBlock(b, d);
|
|
123
|
+
return b.replace(/verificado con `dai check` ✅/g, `verificado con \`dai check\`: ${EMOJI[d.status] || d.status}`);
|
|
34
124
|
}
|
|
35
125
|
|
|
36
126
|
// PR de una branch exenta (chore/, docs/, release/…): no implementa una US y no se le
|
|
37
127
|
// exige link. Lo que NO puede pasar es que salga con la US de otro ni con el placeholder
|
|
38
128
|
// `ABC-###` del template — las dos cosas pasaron en repos reales (issues #31, #33).
|
|
39
129
|
// Se dice explícitamente que no hay US, y por qué.
|
|
40
|
-
function
|
|
41
|
-
|
|
130
|
+
function fillNoUsHeader(template, d) {
|
|
131
|
+
return replaceSection(template, "🔗 Implementa",
|
|
42
132
|
`- **Sin US:** esta PR no implementa una User Story.\n` +
|
|
43
133
|
(d.noUsReason ? `- **Motivo:** ${d.noUsReason}.\n` : "") +
|
|
44
134
|
`- No se le exige link (\`governance/branch-naming.md\`).`);
|
|
45
|
-
if (d.commits && d.commits.length) {
|
|
46
|
-
b = replaceSection(b, "Cambios realizados", d.commits.map((c) => `- [x] ${c}`).join("\n"));
|
|
47
|
-
}
|
|
48
|
-
return upsertLinksBlock(b, d);
|
|
49
135
|
}
|
|
50
136
|
|
|
51
137
|
// ── Bloque de enlaces ────────────────────────────────────────────────────────
|
|
@@ -239,7 +239,9 @@ código + spec trazable) — y la asigna a un partner:
|
|
|
239
239
|
```
|
|
240
240
|
$ dai check
|
|
241
241
|
✅ ABC-482 al día (v1)
|
|
242
|
-
$ dai pr --assignee mgomez
|
|
242
|
+
$ dai pr --assignee mgomez \
|
|
243
|
+
--description "Permite comprar sin crear cuenta: el checkout acepta un email de
|
|
244
|
+
contacto y genera la orden como invitado. Baja el abandono del paso 2."
|
|
243
245
|
✓ PR #123 creada → …/pull/123 (base: main · US: ABC-482 @ v1 · dai check ✅)
|
|
244
246
|
```
|
|
245
247
|
|
package/docs/guias/dev.md
CHANGED
|
@@ -35,8 +35,11 @@
|
|
|
35
35
|
(anti vibe-coding). Ajustas y **commiteas** lo que haga falta.
|
|
36
36
|
6. **Creas la PR** → con el smoke verde y **todo commiteado** (lo que quede sin commitear
|
|
37
37
|
**no entra** en la PR), corres `dai check` (gate: ¿al día con la US?) y en verde `dai pr`
|
|
38
|
-
arma la PR **precargada** (US + estado del check + links; los **dos activos**:
|
|
39
|
-
spec trazable) y la asignas a un partner.
|
|
38
|
+
arma la PR **precargada** (US + estado del check + commits + links; los **dos activos**:
|
|
39
|
+
código + spec trazable) y la asignas a un partner. **La descripción la escribes tú**
|
|
40
|
+
(`--description "…"`, o `--description-file notas.md`): dai llena lo que puede derivar,
|
|
41
|
+
pero no inventa el propósito de un cambio — y si esa sección quedaría con el molde del
|
|
42
|
+
template, **no publica la PR**. Una PR sin descripción no se puede revisar.
|
|
40
43
|
7. **Review de un partner** → un compañero revisa tu PR y **firma** aprobación/rechazo
|
|
41
44
|
(Art. 5). Se apoya en la skill `/dai-review` para un primer pase: un **review inline**
|
|
42
45
|
(resumen + un comentario por línea), que le muestra el preview y espera su OK antes de postear.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dforce2055/dai",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.2",
|
|
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/",
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
"cli/dai.mjs",
|
|
28
28
|
"cli/lib",
|
|
29
29
|
"docs",
|
|
30
|
+
"!docs/public/tutoriales",
|
|
30
31
|
"templates",
|
|
31
32
|
"governance",
|
|
32
33
|
"skills",
|
|
@@ -21,10 +21,19 @@
|
|
|
21
21
|
|
|
22
22
|
## Descripción
|
|
23
23
|
|
|
24
|
-
<!--
|
|
24
|
+
<!--
|
|
25
|
+
Breve propósito de este PR, en términos de negocio (2–4 líneas).
|
|
26
|
+
Lo precarga `dai pr --description "…"` (o `--description-file <archivo.md>`).
|
|
27
|
+
dai NO lo inventa: si esta sección queda sin llenar, `dai pr` no publica la PR.
|
|
28
|
+
-->
|
|
25
29
|
|
|
26
30
|
## Cambios realizados
|
|
27
31
|
|
|
32
|
+
<!--
|
|
33
|
+
Lo precarga `dai pr` con los commits de la branch; `--changes` / `--changes-file`
|
|
34
|
+
lo reemplazan por el detalle que quieras contar.
|
|
35
|
+
-->
|
|
36
|
+
|
|
28
37
|
- [ ] Cambio 1
|
|
29
38
|
- [ ] Cambio 2
|
|
30
39
|
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|