@dforce2055/dai 0.11.0 → 0.12.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 +61 -0
- package/README.md +1 -1
- package/VERSION +1 -1
- package/cli/dai.mjs +62 -17
- package/cli/lib/branch-scope.mjs +61 -5
- package/cli/lib/pr.mjs +21 -1
- 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/tutoriales/index.md +9 -0
- package/docs/tutoriales/setup-dev.md +516 -0
- package/docs/tutoriales/setup-funcional.md +480 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,66 @@
|
|
|
3
3
|
Formato basado en [Keep a Changelog](https://keepachangelog.com/). Versionado semver
|
|
4
4
|
(ver `VERSION`).
|
|
5
5
|
|
|
6
|
+
## [0.12.0] — 2026-08-13
|
|
7
|
+
|
|
8
|
+
**La PR deja de robarle la US a otro. `dai pr` resolvía el link recorriendo todo el repo y
|
|
9
|
+
quedándose con el último `implements.yaml`; ahora lo resuelve la rama, que es la que sabe la
|
|
10
|
+
respuesta — y cuando no puede saberlo, pregunta en vez de elegir en silencio. Más el tutorial
|
|
11
|
+
de setup del dev, la contraparte del que ya tenía el funcional.**
|
|
12
|
+
|
|
13
|
+
### Arreglado
|
|
14
|
+
- **`dai pr` armaba la PR con la US equivocada** (issues [#31](https://github.com/dforce2055/dai/issues/31),
|
|
15
|
+
[#32](https://github.com/dforce2055/dai/issues/32), [#33](https://github.com/dforce2055/dai/issues/33)).
|
|
16
|
+
Recorría **todos** los `implements.yaml` del repo —archivados incluidos, porque era el
|
|
17
|
+
único comando que no pasaba `{ includeArchived: false }`— y el `break` cortaba solo el
|
|
18
|
+
bucle interno, así que ganaba el **último** en orden de lectura. La rama, que la nombra el
|
|
19
|
+
propio `dai link-us`, no entraba en la decisión.
|
|
20
|
+
El síntoma es silencioso y por eso duele: la PR sale con el título, el link y el
|
|
21
|
+
`dai check ✅` de **otra** US. En repos reales convivieron dos PRs con el mismo título y
|
|
22
|
+
contenidos que no tenían nada que ver, y una PR de archivado apareció rotulada con la
|
|
23
|
+
historia de un compañero. Es exactamente el modo de falla que la constitución quiere
|
|
24
|
+
evitar — el link QUÉ↔CÓMO queda mal y nadie se entera, porque **nadie lee el
|
|
25
|
+
`implements.yaml` en la lista de PRs: leen el título**.
|
|
26
|
+
Ahora decide `prScope` (`cli/lib/branch-scope.mjs`), hermana de `stampScope`: la rama
|
|
27
|
+
nombra una US viva → esa; una sola US viva → esa; varias candidatas → **pregunta** con TTY
|
|
28
|
+
y **falla** sin TTY, listándolas.
|
|
29
|
+
- **Una rama exenta ya no hereda la US del repo.** Un `chore/`/`docs/`/`release/` que no
|
|
30
|
+
nombra ninguna US genera la PR **sin** US —título del último commit y la sección
|
|
31
|
+
*Implementa* diciendo que no hay historia— en lugar de colgarle la de otro o dejar el
|
|
32
|
+
placeholder `ABC-###` del template.
|
|
33
|
+
- **`dai done` anunciaba `US cerrada:` con todas las US del repo**, archivadas incluidas.
|
|
34
|
+
Cerrar una rama informaba el cierre de medio sprint. Mismo defecto de clase, en un mensaje.
|
|
35
|
+
- **Ctrl+D en las preguntas de `dai pr`** cancela en vez de cortar con `Aborted with Ctrl+D`.
|
|
36
|
+
Todas ellas preceden a una acción hacia afuera (push + PR): ahí abortar es lo seguro.
|
|
37
|
+
|
|
38
|
+
### Agregado
|
|
39
|
+
- **`dai pr --us <ID>`** — el escape hatch explícito, y la única salida cuando hay ambigüedad
|
|
40
|
+
y no hay TTY (un pipeline). Acepta también un change ya archivado.
|
|
41
|
+
- **El preview dice de dónde salió la US**: `US: ABC-482 — la branch '…' nombra ABC-482`.
|
|
42
|
+
Cuando el título está mal, es lo único que lo delata.
|
|
43
|
+
- **[Tutorial de setup para desarrolladores (Windows)](docs/tutoriales/setup-dev.md)** — el
|
|
44
|
+
otro lado del que ya existía para el funcional: Node, dai, git + SSH + `glab`, las skills
|
|
45
|
+
en Copilot, OpenSpec, el `.env.dai` contra Jira, y el ciclo completo sobre una US real
|
|
46
|
+
(`link-us` → `check` → `mr` → `stamp` → `done`). El troubleshooting sale de lo que pasó de
|
|
47
|
+
verdad en Windows corporativo: el push HTTPS que necesita completar el credential manager,
|
|
48
|
+
el proxy con su propio certificado, el gate de CI.
|
|
49
|
+
|
|
50
|
+
### Versionado
|
|
51
|
+
|
|
52
|
+
**Minor → 0.12.0.** `dai pr` **cambia de comportamiento**: donde antes elegía una US en
|
|
53
|
+
silencio, ahora pregunta (o falla), y una rama exenta genera la PR sin US. En el papel es
|
|
54
|
+
incompatible; en la práctica el comportamiento viejo era el bug de los issues #31/#32/#33.
|
|
55
|
+
Suma la flag `--us`, aditiva. El contrato del modelo (`ac_hash`, schema de `implements.yaml`)
|
|
56
|
+
queda intacto.
|
|
57
|
+
|
|
58
|
+
### Interno
|
|
59
|
+
- **319 tests** (+12 desde 0.11.0): `prScope` ×10 —la rama manda sobre el orden de
|
|
60
|
+
directorio, los archivados no compiten, una `chore/` no hereda, la ambigüedad no se
|
|
61
|
+
resuelve sola— y el cuerpo/título de una PR sin US ×2.
|
|
62
|
+
- `requiresLink()` devuelve además `kind` (`always` / `exempt` / `untyped`): es lo que separa
|
|
63
|
+
"exenta por tipo" de "la rama no dice nada", y lo que `dai pr` necesitaba para no heredar
|
|
64
|
+
la US de otro sin romper el gate de CI.
|
|
65
|
+
|
|
6
66
|
## [0.11.0] — 2026-07-22
|
|
7
67
|
|
|
8
68
|
**Ronda de fixes reportados usándola, más el eslabón que faltaba: editar el QUÉ. `dai stamp`
|
|
@@ -551,6 +611,7 @@ ClickUp y Jira Cloud.
|
|
|
551
611
|
- Tests de las rutas de red (jira/clickup/forge) con `fetch` mockeado. Sin links rotos;
|
|
552
612
|
`files` de npm sin tests ni secretos.
|
|
553
613
|
|
|
614
|
+
[0.12.0]: https://github.com/dforce2055/dai/releases/tag/v0.12.0
|
|
554
615
|
[0.11.0]: https://github.com/dforce2055/dai/releases/tag/v0.11.0
|
|
555
616
|
[0.10.0]: https://github.com/dforce2055/dai/releases/tag/v0.10.0
|
|
556
617
|
[0.9.0]: https://github.com/dforce2055/dai/releases/tag/v0.9.0
|
package/README.md
CHANGED
|
@@ -201,7 +201,7 @@ flowchart TD
|
|
|
201
201
|
| `dai link-us <ID> --resync` | re-estampa el `ac_hash` contra la US viva (tras un ⚠️ de check) |
|
|
202
202
|
| `dai check` | compara tu código vs la US viva → ✅ al día / ⚠️ atrasado (exit code = gate de PR) |
|
|
203
203
|
| `dai ls [--json]` | lista las US que implementa el repo + su link al tracker |
|
|
204
|
-
| `dai pr [--assignee u] [--base b] [--draft] [--yes]` · 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 |
|
|
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
205
|
| `dai stamp` | estampa la cobertura inversa en el tracker (branch + commit-ancla) |
|
|
206
206
|
| `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
207
|
| `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` |
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.12.0
|
package/cli/dai.mjs
CHANGED
|
@@ -35,7 +35,7 @@ import { parseSource } from "./lib/skills-source.mjs";
|
|
|
35
35
|
import { skillToCursor, validateSkill, constitution, constitutionCursorRule, envFor, mergeEnv, upsertBlock, reconcileGitignore, stalePromptFiles } from "./lib/bootstrap.mjs";
|
|
36
36
|
import { parseFieldsFile, parseFieldOverrides, resolveJiraFields } from "./lib/jira-fields.mjs";
|
|
37
37
|
import { assertProjectKey } from "./lib/pm-jira.mjs";
|
|
38
|
-
import { flattenImplements, stampScope, requiresLink, trackerKeysIn } from "./lib/branch-scope.mjs";
|
|
38
|
+
import { flattenImplements, stampScope, prScope, matchBranchToImplements, requiresLink, trackerKeysIn } from "./lib/branch-scope.mjs";
|
|
39
39
|
import { describeForgeError, parseForgeError } from "./lib/forge-api.mjs";
|
|
40
40
|
import { validateUS, renderValidation, parseSpecVersion, bumpSpecVersion, setSpecVersion } from "./lib/us-format.mjs";
|
|
41
41
|
|
|
@@ -802,8 +802,12 @@ function cmdDone(opts) {
|
|
|
802
802
|
}
|
|
803
803
|
|
|
804
804
|
// La US que se cierra (informativo) — leerla ANTES de cambiar de branch.
|
|
805
|
-
|
|
806
|
-
|
|
805
|
+
// Solo la de ESTA branch: antes listaba todas las del repo (archivadas incluidas), así
|
|
806
|
+
// que cerrar una branch anunciaba el cierre de medio sprint — el mismo defecto que
|
|
807
|
+
// `dai pr` en los issues #31/#32/#33, acá en un mensaje.
|
|
808
|
+
const usIds = matchBranchToImplements(
|
|
809
|
+
branch, flattenImplements(discoverImplements(process.cwd(), { includeArchived: false })),
|
|
810
|
+
).map((r) => r.id);
|
|
807
811
|
|
|
808
812
|
// Ir a la base y actualizar.
|
|
809
813
|
info(`Cambiando a '${base}' y actualizando…`);
|
|
@@ -848,7 +852,10 @@ async function cmdPr(opts) {
|
|
|
848
852
|
const ask = async (q) => {
|
|
849
853
|
if (!process.stdin.isTTY) return null; // no interactivo → sin preguntas
|
|
850
854
|
if (!_rl) _rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
851
|
-
|
|
855
|
+
// Ctrl+D (EOF) → "" = cancelar, no un crash: todas estas preguntas preceden a una
|
|
856
|
+
// acción hacia afuera (push + PR), y ahí abortar es la respuesta segura.
|
|
857
|
+
try { return (await _rl.question(q)).trim(); }
|
|
858
|
+
catch { process.stdout.write("\n"); return ""; }
|
|
852
859
|
};
|
|
853
860
|
const closeRl = () => { if (_rl) { _rl.close(); _rl = null; } };
|
|
854
861
|
|
|
@@ -863,19 +870,53 @@ async function cmdPr(opts) {
|
|
|
863
870
|
fail(`no hay commits en '${branch}' por encima de '${base}'. Una PR necesita cambios: haz commit primero (git commit).`, 1);
|
|
864
871
|
}
|
|
865
872
|
|
|
866
|
-
// 1. Resolver el link (US) de
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
873
|
+
// 1. Resolver el link (US) de ESTA branch (issues #31, #32, #33).
|
|
874
|
+
// Antes se recorrían todos los implements.yaml del repo —archivados incluidos— y
|
|
875
|
+
// ganaba el último: la branch, que es la que sabe la respuesta, no entraba en la
|
|
876
|
+
// decisión. La PR salía titulada con la historia de otro y nadie se enteraba,
|
|
877
|
+
// porque el `dai check ✅` del body era el de esa otra US.
|
|
878
|
+
const rows = flattenImplements(discoverImplements(process.cwd(), { includeArchived: false }));
|
|
879
|
+
const allRows = flattenImplements(discoverImplements(process.cwd()));
|
|
880
|
+
const scope = prScope({ branch, rows, allRows, ids: opts.us ? [String(opts.us)] : [] });
|
|
881
|
+
let entry = scope.target;
|
|
882
|
+
|
|
883
|
+
if (scope.mode === "explicit" && !entry) {
|
|
884
|
+
closeRl();
|
|
885
|
+
fail(`no encontré implements.yaml para '${opts.us}'.\n` +
|
|
886
|
+
` US en este repo: ${allRows.map((r) => r.id).join(", ") || "(ninguna)"}`, 1);
|
|
887
|
+
}
|
|
888
|
+
if (scope.mode === "none") {
|
|
889
|
+
closeRl();
|
|
890
|
+
fail("no hay una US linkeada (implements.yaml). Si este PR implementa una US, corré `dai link-us` primero. Si es un chore/tooling (sin US), renombrá la branch a `chore/…` o creá la PR con tu forge: `glab mr create` / `gh pr create`.", 1);
|
|
871
891
|
}
|
|
872
|
-
if (
|
|
873
|
-
|
|
892
|
+
if (scope.mode === "ambiguous") {
|
|
893
|
+
// Elegir en silencio es EL bug: la PR sale publicada con el link QUÉ↔CÓMO de otra US.
|
|
894
|
+
warn(`${scope.reason}.`);
|
|
895
|
+
const listed = scope.candidates.map((r, i) => ` ${i + 1}) ${r.id} ${C.dim(`(${r.change})`)}`).join("\n");
|
|
896
|
+
process.stdout.write(` US vivas en el repo:\n${listed}\n`);
|
|
897
|
+
if (opts.yes || !process.stdin.isTTY) {
|
|
898
|
+
closeRl();
|
|
899
|
+
fail("no sé con qué US titular la PR. Decilo explícitamente: dai pr --us <ID>", 1);
|
|
900
|
+
}
|
|
901
|
+
const ans = await ask(` ¿Con qué US titulo la PR? (número, Enter=cancelar) `);
|
|
902
|
+
if (!ans) { closeRl(); info("Cancelado — no se creó la PR."); return; }
|
|
903
|
+
const n = Number(ans);
|
|
904
|
+
if (!Number.isInteger(n) || n < 1 || n > scope.candidates.length) {
|
|
905
|
+
closeRl();
|
|
906
|
+
fail(`respuesta inválida: '${ans}'. Se esperaba un número entre 1 y ${scope.candidates.length}.`, 1);
|
|
907
|
+
}
|
|
908
|
+
entry = scope.candidates[n - 1];
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
// De dónde salió la US: en el preview, el título sin justificación no delata nada
|
|
912
|
+
// cuando está mal (sugerencia del issue #32).
|
|
913
|
+
const usWhy = entry ? (scope.mode === "ambiguous" ? "la elegiste vos" : scope.reason) : scope.reason;
|
|
914
|
+
const { id, version, ac_hash } = entry || {};
|
|
874
915
|
|
|
875
916
|
// 2. Estado de trazabilidad (dai check) contra la US viva.
|
|
876
917
|
const adapter = getAdapter(process.env);
|
|
877
|
-
const live = await Promise.resolve(adapter.fetchUS(id)).catch(() => null);
|
|
878
|
-
const status = coverageStatus(ac_hash, live?.ac_hash);
|
|
918
|
+
const live = id ? await Promise.resolve(adapter.fetchUS(id)).catch(() => null) : null;
|
|
919
|
+
const status = id ? coverageStatus(ac_hash, live?.ac_hash) : null;
|
|
879
920
|
if (status === "atrasado") {
|
|
880
921
|
warn(`la US ${id} está ATRASADA respecto de tu implementación (${ac_hash} ≠ ${live?.ac_hash}).`);
|
|
881
922
|
warn(`resincroniza antes de abrir la PR: dai link-us ${id} --resync`);
|
|
@@ -889,22 +930,25 @@ async function cmdPr(opts) {
|
|
|
889
930
|
let commits = [];
|
|
890
931
|
try { commits = git(["log", `${base}..HEAD`, "--pretty=%s"]).split("\n").filter(Boolean); } catch { /* base local ausente */ }
|
|
891
932
|
// La canónica del tracker (live.url) gana sobre la derivada; el template gana sobre todo.
|
|
892
|
-
const usUrl = usUrlFor(id, live?.url);
|
|
893
|
-
if (!usUrl) {
|
|
933
|
+
const usUrl = id ? usUrlFor(id, live?.url) : null;
|
|
934
|
+
if (id && !usUrl) {
|
|
894
935
|
warn(`no sé la URL de ${id} en el tracker: la PR va a quedar sin link a la US.`);
|
|
895
936
|
warn(`configurá DAI_TRACKER_URL_TEMPLATE en el .env.dai (p. ej. https://tu-tracker/browse/{id}).`);
|
|
896
937
|
}
|
|
897
938
|
const body = composePrBody(readFileSync(tplPath, "utf8"), {
|
|
898
|
-
id, version, ac_hash, status, usUrl, usTitle: live?.title, commits,
|
|
939
|
+
id, version, ac_hash, status, usUrl, usTitle: live?.title, commits, noUsReason: id ? null : scope.reason,
|
|
899
940
|
branch, branchUrl: branchUrl(remote, branch), commit, commitUrl: commitUrl(remote, commit),
|
|
900
941
|
});
|
|
901
|
-
|
|
942
|
+
// Sin US el título sale del último commit: describe lo que hay adentro, en vez de
|
|
943
|
+
// pedirle prestada la historia a otro (issue #31).
|
|
944
|
+
const title = prTitle(opts, id, live?.title, commits[0] || branch);
|
|
902
945
|
const forge = detectForge(parseRemote(remote)?.host);
|
|
903
946
|
const tool = forgeTool(forge);
|
|
904
947
|
|
|
905
948
|
// 4. Mostrar y pedir confirmación (acción hacia afuera).
|
|
906
949
|
process.stdout.write(`\n ── Pull Request a crear ──────────────────────────────\n`);
|
|
907
950
|
process.stdout.write(` título: ${title}\n de: ${branch}\n a: ${base}\n`);
|
|
951
|
+
process.stdout.write(` US: ${id || C.dim("(sin US)")} ${C.dim(`— ${usWhy}`)}\n`);
|
|
908
952
|
process.stdout.write(` forge: ${forge} (${tool})${opts.assignee ? `\n asignar: ${opts.assignee}` : ""}${opts.draft ? "\n draft: sí" : ""}\n`);
|
|
909
953
|
process.stdout.write(` ─────────────────────────────────────────────────────\n\n${body}\n`);
|
|
910
954
|
process.stdout.write(` ─────────────────────────────────────────────────────\n`);
|
|
@@ -1699,6 +1743,7 @@ switch (cmd) {
|
|
|
1699
1743
|
" done [--base main] [--force] cierra la US: vuelve a la base, actualiza y borra la branch local (si está mergeada)\n" +
|
|
1700
1744
|
" 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" +
|
|
1701
1745
|
" pr (alias mr) [--assignee u] [--base b] [--draft] [--yes] crea TU PR/MR precargada (muestra + confirma)\n" +
|
|
1746
|
+
" [--us <ID>] [--title t] la US la resuelve la branch; si hay varias, pregunta (sin TTY, falla)\n" +
|
|
1702
1747
|
" forge comment <ref> --body-file <f> · forge pr <ref> comentar/leer una PR ajena (github/gitlab)\n" +
|
|
1703
1748
|
" forge review <ref> --from <review.json> [--dry-run|--yes] review inline: resumen + comentario por línea\n" +
|
|
1704
1749
|
" --min-severity low|medium|high · --min-confidence 0..1 · --max-comments N · --base <branch>\n" +
|
package/cli/lib/branch-scope.mjs
CHANGED
|
@@ -56,17 +56,22 @@ export function trackerKeysIn(branch) {
|
|
|
56
56
|
// chore/, docs/, ci/… → nunca
|
|
57
57
|
// fix/ y el resto → solo si el nombre trae un ID con pinta de key de tracker
|
|
58
58
|
// `main`/`develop`/sin branch → no (no es una branch de trabajo).
|
|
59
|
+
//
|
|
60
|
+
// `kind` distingue POR QUÉ no se exige, que no es lo mismo para todos los comandos:
|
|
61
|
+
// una `chore/` está exenta POR TIPO (el repo declara que ahí no hay producto), mientras
|
|
62
|
+
// que `mi-branch` simplemente no dice nada. `dai pr` usa esa diferencia para no colgarle
|
|
63
|
+
// la US viva del repo a una PR de archivado (issue #31).
|
|
59
64
|
export function requiresLink(branch) {
|
|
60
65
|
const t = branchType(branch);
|
|
61
|
-
if (ALWAYS.has(t)) return { required: true, reason: `'${t}/' es trabajo de producto: requiere US` };
|
|
62
|
-
if (EXEMPT.has(t)) return { required: false, reason: `'${t}/' está exenta de US (governance/branch-naming.md)` };
|
|
63
|
-
if (t === "") return { required: false, reason: "sin prefijo tipo/, no es una branch de trabajo: sin gate de link" };
|
|
66
|
+
if (ALWAYS.has(t)) return { required: true, kind: "always", reason: `'${t}/' es trabajo de producto: requiere US` };
|
|
67
|
+
if (EXEMPT.has(t)) return { required: false, kind: "exempt", reason: `'${t}/' está exenta de US (governance/branch-naming.md)` };
|
|
68
|
+
if (t === "") return { required: false, kind: "untyped", reason: "sin prefijo tipo/, no es una branch de trabajo: sin gate de link" };
|
|
64
69
|
// Tipo desconocido (fix/, spike/, lo que el repo use): si nombró un ID, lo tomamos
|
|
65
70
|
// como intención de implementar una US y se lo exigimos. Si no, no inventamos.
|
|
66
71
|
const keyish = trackerKeysIn(branch).length > 0;
|
|
67
72
|
return keyish
|
|
68
|
-
? { required: true, reason: `'${t}/' con un ID en el nombre: se toma como trabajo de producto` }
|
|
69
|
-
: { required: false, reason: `'${t}/' sin ID en el nombre: no exige US (governance/branch-naming.md)` };
|
|
73
|
+
? { required: true, kind: "always", reason: `'${t}/' con un ID en el nombre: se toma como trabajo de producto` }
|
|
74
|
+
: { required: false, kind: "untyped", reason: `'${t}/' sin ID en el nombre: no exige US (governance/branch-naming.md)` };
|
|
70
75
|
}
|
|
71
76
|
|
|
72
77
|
// Aplana los implements descubiertos a filas { path, change, repo, id, version, ac_hash },
|
|
@@ -142,3 +147,54 @@ export function stampScope({ branch, rows, allRows = rows, ids = [], all = false
|
|
|
142
147
|
reason: `hay ${hit.length > 1 ? hit.length : rows.length} US vivas y la branch '${branch}' no dice cuál`,
|
|
143
148
|
};
|
|
144
149
|
}
|
|
150
|
+
|
|
151
|
+
// La decisión de `dai pr`: ¿con qué US se titula y se linkea ESTA PR?
|
|
152
|
+
//
|
|
153
|
+
// { mode, target, candidates, reason, missing }
|
|
154
|
+
//
|
|
155
|
+
// mode "explicit" → el id que pidió el usuario (dai pr --us ABC-482)
|
|
156
|
+
// mode "branch" → la branch nombra una US viva del repo: esa
|
|
157
|
+
// mode "only" → hay una sola US viva y la branch no está exenta: esa
|
|
158
|
+
// mode "exempt" → branch exenta por tipo (chore/, docs/…) que no nombra US: PR SIN US
|
|
159
|
+
// mode "ambiguous" → varias candidatas y ninguna pista: NO elige, pregunta
|
|
160
|
+
// mode "none" → la branch pide US y el repo no tiene ninguna viva
|
|
161
|
+
//
|
|
162
|
+
// Es la misma pregunta que resuelve stampScope, con dos diferencias que importan
|
|
163
|
+
// (issues #31, #32, #33 — antes `dai pr` recorría TODOS los implements.yaml del repo,
|
|
164
|
+
// archivados incluidos, y se quedaba con el último):
|
|
165
|
+
//
|
|
166
|
+
// 1. Una PR implementa UNA US, no cuatro: no hay modo "all", y el resultado es un
|
|
167
|
+
// único `target`.
|
|
168
|
+
// 2. Una branch exenta NO hereda la US viva del repo. Una PR de `chore/archive-specs`
|
|
169
|
+
// titulada con la historia de un compañero es peor que una sin título lindo: la
|
|
170
|
+
// lista de PRs es la única superficie donde el equipo lee la trazabilidad.
|
|
171
|
+
//
|
|
172
|
+
// `rows` viene del discover SIN archivados; `allRows` incluye los archivados y solo se
|
|
173
|
+
// usa para resolver un id explícito (reabrir la PR de un change ya archivado) y para
|
|
174
|
+
// listar qué US conoce el repo cuando el id pedido no está.
|
|
175
|
+
export function prScope({ branch, rows, allRows = rows, ids = [] }) {
|
|
176
|
+
if (ids.length) {
|
|
177
|
+
const want = String(ids[0]).toLowerCase();
|
|
178
|
+
const target = allRows.find((r) => String(r.id).toLowerCase() === want) || null;
|
|
179
|
+
// `missing` conserva la grafía del usuario: devolverle su ABC-404 en minúsculas lo
|
|
180
|
+
// manda a dudar del case en vez de del id.
|
|
181
|
+
return { mode: "explicit", target, candidates: allRows, missing: target ? [] : [ids[0]], reason: `pediste --us ${ids[0]}` };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const hit = matchBranchToImplements(branch, rows);
|
|
185
|
+
if (hit.length === 1) {
|
|
186
|
+
return { mode: "branch", target: hit[0], candidates: rows, reason: `la branch '${branch}' nombra ${hit[0].id}` };
|
|
187
|
+
}
|
|
188
|
+
if (hit.length > 1) {
|
|
189
|
+
return { mode: "ambiguous", target: null, candidates: hit, reason: `la branch '${branch}' nombra ${hit.length} US vivas` };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// La branch no nombra ninguna US viva.
|
|
193
|
+
const req = requiresLink(branch);
|
|
194
|
+
if (req.kind === "exempt") {
|
|
195
|
+
return { mode: "exempt", target: null, candidates: rows, reason: `${req.reason} y su nombre no nombra ninguna US` };
|
|
196
|
+
}
|
|
197
|
+
if (rows.length === 0) return { mode: "none", target: null, candidates: [], reason: "no hay implements.yaml vivo en el repo" };
|
|
198
|
+
if (rows.length === 1) return { mode: "only", target: rows[0], candidates: rows, reason: "es la única US viva del repo" };
|
|
199
|
+
return { mode: "ambiguous", target: null, candidates: rows, reason: `hay ${rows.length} US vivas y la branch '${branch}' no dice cuál` };
|
|
200
|
+
}
|
package/cli/lib/pr.mjs
CHANGED
|
@@ -18,6 +18,7 @@ export function replaceSection(body, heading, content) {
|
|
|
18
18
|
// placeholders que encuentra y deja el resto para que el humano lo edite.
|
|
19
19
|
export function composePrBody(template, d) {
|
|
20
20
|
let b = template;
|
|
21
|
+
if (!d.id) return composePrBodyNoUs(b, d);
|
|
21
22
|
b = b.replace(/`ABC-###`/g, `\`${d.id}\``);
|
|
22
23
|
b = b.replace(/@ `vX`/g, `@ \`${d.version}\``);
|
|
23
24
|
b = b.replace(/`<hash>`/g, `\`${d.ac_hash}\``);
|
|
@@ -32,6 +33,21 @@ export function composePrBody(template, d) {
|
|
|
32
33
|
return upsertLinksBlock(b, d);
|
|
33
34
|
}
|
|
34
35
|
|
|
36
|
+
// PR de una branch exenta (chore/, docs/, release/…): no implementa una US y no se le
|
|
37
|
+
// exige link. Lo que NO puede pasar es que salga con la US de otro ni con el placeholder
|
|
38
|
+
// `ABC-###` del template — las dos cosas pasaron en repos reales (issues #31, #33).
|
|
39
|
+
// Se dice explícitamente que no hay US, y por qué.
|
|
40
|
+
function composePrBodyNoUs(template, d) {
|
|
41
|
+
let b = replaceSection(template, "🔗 Implementa",
|
|
42
|
+
`- **Sin US:** esta PR no implementa una User Story.\n` +
|
|
43
|
+
(d.noUsReason ? `- **Motivo:** ${d.noUsReason}.\n` : "") +
|
|
44
|
+
`- 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
|
+
}
|
|
50
|
+
|
|
35
51
|
// ── Bloque de enlaces ────────────────────────────────────────────────────────
|
|
36
52
|
// Delimitado y regenerable a propósito. Con el comentario suelto de antes, cualquier
|
|
37
53
|
// agente que reescribiera "Enlaces relacionados" se lo llevaba puesto sin dejar rastro
|
|
@@ -70,8 +86,12 @@ export function upsertLinksBlock(body, d) {
|
|
|
70
86
|
}
|
|
71
87
|
|
|
72
88
|
// Título del PR: el pasado a mano, o "<ID>: <título de la US>", o solo el ID.
|
|
73
|
-
|
|
89
|
+
// Sin US (branch exenta) cae al `fallback` — el subject del último commit: describe
|
|
90
|
+
// lo que hay adentro. Inventar un título con la US de otro es el bug de los issues
|
|
91
|
+
// #31/#33, y el título es lo ÚNICO que se ve en la lista de PRs.
|
|
92
|
+
export function prTitle(opts, id, usTitle, fallback = "") {
|
|
74
93
|
if (opts.title) return opts.title;
|
|
94
|
+
if (!id) return fallback;
|
|
75
95
|
if (usTitle) return `${id}: ${usTitle}`;
|
|
76
96
|
return id;
|
|
77
97
|
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/docs/tutoriales/index.md
CHANGED
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
Guías de **setup operativo** — lo que haces una vez por máquina para trabajar con dai.
|
|
4
4
|
|
|
5
|
+
## De cero a publicar
|
|
6
|
+
|
|
7
|
+
- [**Setup para analistas funcionales y PMs (Windows)**](./setup-funcional) — el camino
|
|
8
|
+
completo sin git ni repositorio: Node, dai, las skills en Copilot, el token de Jira y una
|
|
9
|
+
épica + US de prueba publicadas de verdad.
|
|
10
|
+
- [**Setup para desarrolladores (Windows)**](./setup-dev) — el otro lado: Node, dai, git + SSH
|
|
11
|
+
+ `glab`, las skills en Copilot, OpenSpec y el ciclo completo sobre una US real
|
|
12
|
+
(`link-us` → `check` → `mr` → `stamp`).
|
|
13
|
+
|
|
5
14
|
## Preparar el entorno
|
|
6
15
|
|
|
7
16
|
- [**Configurar git**](./configurar-git) — tu identidad (nombre + correo) para que los
|