@dforce2055/dai 0.8.1 → 0.8.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/.env.example CHANGED
@@ -6,8 +6,11 @@
6
6
  # md | jira | clickup
7
7
  DAI_PM=md
8
8
 
9
- # Plantilla del link al ticket (para `dai ls` / stamp). {id} se reemplaza.
10
- DAI_TRACKER_URL_TEMPLATE=https://jira.miempresa.com/browse/{id}
9
+ # OPCIONAL. Plantilla del link al ticket (`dai ls`, `dai pr`, stamp). {id} se reemplaza.
10
+ # Con DAI_PM=jira o =clickup, dai ya deduce el link solo: esto es un OVERRIDE, y solo
11
+ # hace falta si tu tracker vive en otra URL (p. ej. un Jira Server con path propio).
12
+ # Si dai no puede saber el link, avisa y deja la PR sin él — nunca escribe el id pelado.
13
+ # DAI_TRACKER_URL_TEMPLATE=https://jira.miempresa.com/browse/{id}
11
14
 
12
15
  # ── Backend md (local, offline) ──────────────────────────────────────────────
13
16
  # Carpeta donde viven las US como <ID>.md (p. ej. ABC-482.md).
package/CHANGELOG.md CHANGED
@@ -3,6 +3,46 @@
3
3
  Formato basado en [Keep a Changelog](https://keepachangelog.com/). Versionado semver
4
4
  (ver `VERSION`).
5
5
 
6
+ ## [0.8.2] — 2026-07-17
7
+
8
+ **Dos agujeros que destapó el uso real, y que tienen la misma forma: dai hacía algo
9
+ hacia afuera sin que un humano lo viera, o dejaba que otro le pisara lo que había
10
+ escrito. El [Art. 5](docs/MANIFIESTO.md#art-5) no se cumple solo con no clickear
11
+ Approve.**
12
+
13
+ ### Arreglado
14
+ - **`dai-review` posteaba el comentario sin mostrártelo.** La skill componía el review y
15
+ lo publicaba de una: el paso 6 decía *"Postear"* y no había gate. Y el comentario sale
16
+ con **tu token y tu nombre** (`GITHUB_TOKEN`/`GITLAB_TOKEN` son tuyos), así que en la
17
+ PR de un compañero figura como si lo hubieras escrito vos. El corte estaba puesto en el
18
+ lugar equivocado: no aprobar sin humano estaba bien, pero publicar un juicio sobre el
19
+ código de otro, firmado por alguien que no lo leyó, es el mismo problema con otro
20
+ disfraz. Ahora la skill **muestra el comentario entero y espera un OK explícito** en
21
+ ese turno; sin "sí", no se postea. Es el tercer corte duro de la skill.
22
+ - **`dai pr` escribía el id de la US disfrazado de link.** Sin `DAI_TRACKER_URL_TEMPLATE`,
23
+ `trackerUrl(id)` devolvía el **id pelado**; como un string es truthy, `composePrBody` lo
24
+ escribía igual y la PR quedaba con `- US: 86abc123` en vez de un enlace, sin un solo
25
+ aviso. Ahora la URL se resuelve por una cadena explícita —template > URL canónica del
26
+ tracker > derivada del backend > `null`— y **si dai no la sabe, avisa y omite la línea
27
+ en vez de mentir** (`lib/tracker-url.mjs`).
28
+ - **El bloque de enlaces de `dai pr` no sobrevivía a un edit.** Iba marcado con un
29
+ comentario suelto, así que cualquier agente que reescribiera *"Enlaces relacionados"*
30
+ se lo llevaba puesto sin dejar rastro — pasó en PRs reales. Y el propio template lo
31
+ invitaba: su hint pedía *"US en el tracker, commit ancla, docs, issues"*, o sea justo la
32
+ sección que `dai pr` acababa de llenar. dai se peleaba consigo mismo y ganaba el que
33
+ corría último. Ahora el bloque va **delimitado** (`<!-- dai:links:start … end -->`),
34
+ se **regenera de forma idempotente**, preserva lo que el humano sumó abajo, y el hint
35
+ del template pide solo lo que dai **no** sabe (docs, issues, PRs relacionadas).
36
+
37
+ ### Cambiado
38
+ - **dai deduce el link al tracker solo.** Con `DAI_PM=jira` o `=clickup` ya no hace falta
39
+ `DAI_TRACKER_URL_TEMPLATE`: se deriva de la config (`/browse/<KEY>` y
40
+ `/t/<id>`), y `fetchUS` ahora devuelve la **URL canónica** del tracker — en ClickUp, la
41
+ que trae el `team_id`, que no se puede deducir del id. La variable queda como
42
+ **override** para trackers con URL propia. `dai init` dejó de scaffoldearla: era
43
+ contraproducente, porque el template gana sobre la canónica y le tapaba el `team_id`.
44
+ Los `.env` que ya la tienen siguen andando igual (el override sigue ganando).
45
+
6
46
  ## [0.8.1] — 2026-07-16
7
47
 
8
48
  **Primera prueba real en Windows con analistas y devs de una empresa: el ciclo completo
@@ -326,6 +366,8 @@ ClickUp y Jira Cloud.
326
366
  - Tests de las rutas de red (jira/clickup/forge) con `fetch` mockeado. Sin links rotos;
327
367
  `files` de npm sin tests ni secretos.
328
368
 
369
+ [0.8.2]: https://github.com/dforce2055/dai/releases/tag/v0.8.2
370
+ [0.8.1]: https://github.com/dforce2055/dai/releases/tag/v0.8.1
329
371
  [0.8.0]: https://github.com/dforce2055/dai/releases/tag/v0.8.0
330
372
  [0.7.0]: https://github.com/dforce2055/dai/releases/tag/v0.7.0
331
373
  [0.6.0]: https://github.com/dforce2055/dai/releases/tag/v0.6.0
package/VERSION CHANGED
@@ -1 +1 @@
1
- 0.8.1
1
+ 0.8.2
package/cli/dai.mjs CHANGED
@@ -25,6 +25,7 @@ import { loadEnv } from "./lib/env.mjs";
25
25
  import { getAdapter, coverageStatus, statusLabel } from "./lib/pm-adapter.mjs";
26
26
  import { branchUrl, commitUrl, parseRemote, detectForge } from "./lib/forge-url.mjs";
27
27
  import { parsePrRef, getPR, postComment } from "./lib/forge-api.mjs";
28
+ import { trackerUrl } from "./lib/tracker-url.mjs";
28
29
  import { composePrBody, prTitle, forgeTool } from "./lib/pr.mjs";
29
30
  import { dirsEqual } from "./lib/fsutil.mjs";
30
31
  import { parseFlags, parseAssistants, isAssistantToken, asList } from "./lib/args.mjs";
@@ -66,10 +67,9 @@ function runNpmTool(name, args, opts = {}) {
66
67
  const win = process.platform === "win32";
67
68
  return execFileSync(win ? `${name}.cmd` : name, args, { shell: win, ...opts });
68
69
  }
69
- function trackerUrl(id) {
70
- const tpl = process.env.DAI_TRACKER_URL_TEMPLATE;
71
- return tpl ? tpl.replace("{id}", id) : id;
72
- }
70
+ // La URL de la US: template > canónica del tracker > derivada del backend > null.
71
+ // Nunca el id pelado: ver el porqué en lib/tracker-url.mjs.
72
+ const usUrlFor = (id, liveUrl = null) => trackerUrl(id, { env: process.env, liveUrl });
73
73
 
74
74
  // ── ac-hash ─────────────────────────────────────────────────────────────────
75
75
  function cmdAcHash(arg) {
@@ -88,7 +88,7 @@ function cmdLs(opts) {
88
88
  for (const im of f.implements || []) {
89
89
  if (isPlaceholderId(im.id)) continue; // saltea plantillas sin completar
90
90
  rows.push({ change: f.change, repo: f.repo, id: im.id, version: im.version,
91
- ac_hash: im.ac_hash, link: trackerUrl(im.id) });
91
+ ac_hash: im.ac_hash, link: usUrlFor(im.id) });
92
92
  }
93
93
  }
94
94
  if (opts.json) { process.stdout.write(JSON.stringify(rows, null, 2) + "\n"); return; }
@@ -394,8 +394,14 @@ async function cmdPr(opts) {
394
394
  // Commits de la branch (para precargar "Cambios realizados").
395
395
  let commits = [];
396
396
  try { commits = git(["log", `${base}..HEAD`, "--pretty=%s"]).split("\n").filter(Boolean); } catch { /* base local ausente */ }
397
+ // La canónica del tracker (live.url) gana sobre la derivada; el template gana sobre todo.
398
+ const usUrl = usUrlFor(id, live?.url);
399
+ if (!usUrl) {
400
+ warn(`no sé la URL de ${id} en el tracker: la PR va a quedar sin link a la US.`);
401
+ warn(`configurá DAI_TRACKER_URL_TEMPLATE en el .env (p. ej. https://tu-tracker/browse/{id}).`);
402
+ }
397
403
  const body = composePrBody(readFileSync(tplPath, "utf8"), {
398
- id, version, ac_hash, status, usUrl: trackerUrl(id), usTitle: live?.title, commits,
404
+ id, version, ac_hash, status, usUrl, usTitle: live?.title, commits,
399
405
  branch, branchUrl: branchUrl(remote, branch), commit, commitUrl: commitUrl(remote, commit),
400
406
  });
401
407
  const title = prTitle(opts, id, live?.title);
@@ -92,10 +92,15 @@ export function skillToCursor(md) {
92
92
  }
93
93
 
94
94
  // Contenido del .env según el backend de PM elegido (tokens vacíos, a completar).
95
+ //
96
+ // Sin DAI_TRACKER_URL_TEMPLATE a propósito: con jira/clickup, dai deduce el link solo
97
+ // (lib/tracker-url.mjs). Scaffoldearlo era peor que no ponerlo — el template GANA sobre
98
+ // la URL canónica que devuelve el tracker, así que el `/t/{id}` que escribíamos acá
99
+ // tapaba la de ClickUp con team_id. Queda como override manual para trackers raros.
95
100
  export function envFor(pm) {
96
101
  const head = "# Config de dai — completá lo que falte. NUNCA commitees tokens (.env está gitignored).\n";
97
102
  if (pm === "clickup") {
98
- return head + "DAI_PM=clickup\nDAI_CLICKUP_TOKEN=\nDAI_CLICKUP_LIST_ID=\nDAI_TRACKER_URL_TEMPLATE=https://app.clickup.com/t/{id}\n";
103
+ return head + "DAI_PM=clickup\nDAI_CLICKUP_TOKEN=\nDAI_CLICKUP_LIST_ID=\n";
99
104
  }
100
105
  if (pm === "jira") {
101
106
  return head +
@@ -107,8 +112,7 @@ export function envFor(pm) {
107
112
  "DAI_JIRA_PROJECT=\n" +
108
113
  "DAI_JIRA_ISSUETYPE=Story\n" +
109
114
  "# Campos propios que tu Jira exige al crear. Si el archivo no existe, se ignora.\n" +
110
- "DAI_JIRA_FIELDS_FILE=.dai/jira-fields.json\n" +
111
- "DAI_TRACKER_URL_TEMPLATE=\n";
115
+ "DAI_JIRA_FIELDS_FILE=.dai/jira-fields.json\n";
112
116
  }
113
117
  return head + "DAI_PM=md\nDAI_MD_US_DIR=.dai/us\n";
114
118
  }
@@ -7,9 +7,12 @@
7
7
  // clickup — REST v2 (pm-clickup.mjs)
8
8
  //
9
9
  // Interfaz (fetchUS/stamp pueden ser sync o async — el CLI siempre await-ea):
10
- // fetchUS(id) → { id, title, spec_version, ac_hash } | null
10
+ // fetchUS(id) → { id, title, spec_version, ac_hash, url } | null
11
11
  // stamp(id, record) → destino donde quedó la cobertura
12
12
  // kind → nombre del backend
13
+ //
14
+ // `url` es la URL web canónica de la US según el tracker (opcional: null si el backend
15
+ // no la sabe, como md). El CLI la prefiere sobre la derivada — ver tracker-url.mjs.
13
16
 
14
17
  import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
15
18
  import { join, dirname } from "node:path";
@@ -29,7 +29,9 @@ export function clickupAdapter(env) {
29
29
  const res = await fetch(clickupTaskUrl(id), { headers: clickupAuthHeaders(env) });
30
30
  if (res.status === 404) return null;
31
31
  if (!res.ok) throw new Error(`clickup ${res.status}: ${await res.text()}`);
32
- return { id, ...parseUS(clickupTaskToText(await res.json())) };
32
+ const j = await res.json();
33
+ // `url` es la canónica (/t/<team_id>/<id>): la sabe ClickUp, no la deducimos.
34
+ return { id, ...parseUS(clickupTaskToText(j)), url: j.url || null };
33
35
  },
34
36
  async stamp(id, record) {
35
37
  const res = await fetch(clickupCommentUrl(id), {
@@ -143,7 +143,7 @@ export function jiraAdapter(env) {
143
143
  const res = await daiFetch(jiraIssueUrl(base, id), { headers: jiraAuthHeaders(env) });
144
144
  if (res.status === 404) return null;
145
145
  if (!res.ok) throw new Error(`jira ${res.status}: ${await res.text()}`);
146
- return { id, ...parseUS(jiraIssueToText(await res.json())) };
146
+ return { id, ...parseUS(jiraIssueToText(await res.json())), url: `${trim(base)}/browse/${id}` };
147
147
  },
148
148
  async stamp(id, record) {
149
149
  const res = await daiFetch(jiraCommentUrl(base, id), {
package/cli/lib/pr.mjs CHANGED
@@ -29,15 +29,44 @@ export function composePrBody(template, d) {
29
29
  if (d.commits && d.commits.length) {
30
30
  b = replaceSection(b, "Cambios realizados", d.commits.map((c) => `- [x] ${c}`).join("\n"));
31
31
  }
32
- // Bloque de enlaces (lo agrega dai; el humano completa el resto del template).
33
- const links = ["", "<!-- Enlaces precargados por `dai pr` -->"];
34
- if (d.usUrl) links.push(`- US: ${d.usUrl}`);
32
+ return upsertLinksBlock(b, d);
33
+ }
34
+
35
+ // ── Bloque de enlaces ────────────────────────────────────────────────────────
36
+ // Delimitado y regenerable a propósito. Con el comentario suelto de antes, cualquier
37
+ // agente que reescribiera "Enlaces relacionados" se lo llevaba puesto sin dejar rastro
38
+ // (pasó en PRs reales). Con marcadores, el bloque se detecta, se preserva y se
39
+ // regenera — y quien edite el body ve que es de dai y que se pisa solo.
40
+ export const LINKS_START = "<!-- dai:links:start · generado por `dai pr` — no editar a mano -->";
41
+ export const LINKS_END = "<!-- dai:links:end -->";
42
+
43
+ // Los links que dai sabe. Sin URL no inventa la línea: prefiere no decir nada.
44
+ export function renderLinks(d) {
45
+ const links = [LINKS_START];
46
+ if (d.usUrl) links.push(`- US \`${d.id}\`: ${d.usUrl}`);
35
47
  if (d.branchUrl) links.push(`- branch \`${d.branch}\`: ${d.branchUrl}`);
36
48
  if (d.commitUrl) links.push(`- commit \`${(d.commit || "").slice(0, 8)}\`: ${d.commitUrl}`);
37
- return b.replace(/(##\s*Enlaces relacionados\s*\n)(<!--[\s\S]*?-->)?/i,
38
- (m, h) => `${h}${links.join("\n")}\n`) === b
39
- ? b + "\n" + links.join("\n") + "\n" // si no había sección Enlaces, apéndela
40
- : b.replace(/(##\s*Enlaces relacionados\s*\n)(<!--[\s\S]*?-->)?/i, (m, h) => `${h}${links.join("\n")}\n`);
49
+ links.push(LINKS_END);
50
+ return links.join("\n");
51
+ }
52
+
53
+ // Inserta o reemplaza el bloque de dai. Idempotente: correrlo N veces da lo mismo.
54
+ export function upsertLinksBlock(body, d) {
55
+ const block = renderLinks(d);
56
+ // 1. ¿Ya está el bloque delimitado? Se reemplaza entero (regenerar, no duplicar).
57
+ const delimited = /<!--\s*dai:links:start[\s\S]*?dai:links:end\s*-->/i;
58
+ if (delimited.test(body)) return body.replace(delimited, block);
59
+ // 2. ¿Está la sección del template? El bloque va debajo del heading, PRESERVANDO el
60
+ // hint HTML si lo hay: es la guía para quien edite (y es invisible al renderizar).
61
+ // dai suma, no borra — borrar el texto de otro es justo lo que estamos arreglando.
62
+ // (el `\s*` tolera la línea en blanco entre el heading y el hint; como solo matchea
63
+ // espacios, no puede saltar a la sección siguiente para buscarse un comentario)
64
+ const heading = /(^|\n)(##[^\n]*Enlaces relacionados[^\n]*\n)(\s*<!--[\s\S]*?-->[ \t]*\n)?/i;
65
+ if (heading.test(body)) {
66
+ return body.replace(heading, (m, pre, h, hint) => `${pre}${h}${hint || ""}\n${block}\n`);
67
+ }
68
+ // 3. Ni bloque ni sección: se apéndea con su propio heading.
69
+ return `${body.replace(/\s*$/, "")}\n\n## Enlaces relacionados\n\n${block}\n`;
41
70
  }
42
71
 
43
72
  // Título del PR: el pasado a mano, o "<ID>: <título de la US>", o solo el ID.
@@ -0,0 +1,36 @@
1
+ // dai · la URL de la US en el tracker. Puro, sync, sin red (ADR-0007: config por .env).
2
+ //
3
+ // Cadena de resolución, del más específico al más general:
4
+ // 1. DAI_TRACKER_URL_TEMPLATE — override explícito del usuario: siempre gana.
5
+ // 2. La URL que devolvió el tracker en `fetchUS` — la canónica (ClickUp la trae con
6
+ // el team_id; la derivada no puede saberlo). Solo existe si hubo red.
7
+ // 3. Derivada del backend + su config — determinística y offline.
8
+ // 4. `null` — dai NO sabe la URL.
9
+ //
10
+ // El paso 4 es el que importa. Antes devolvíamos el `id` pelado, y como un string es
11
+ // truthy, quien consumía esto lo escribía igual — un id disfrazado de enlace, sin un
12
+ // solo aviso. Preferimos no decir nada antes que mentir: quien llama decide si omite
13
+ // la línea o avisa.
14
+
15
+ // URL web de la US deducida del backend, sin salir a la red.
16
+ export function deriveTrackerUrl(id, env = {}) {
17
+ if (!id) return null;
18
+ const kind = String(env.DAI_PM || "md").toLowerCase();
19
+ if (kind === "clickup") {
20
+ // /t/<id> redirige a la canónica /t/<team_id>/<id>. Sin token no sabemos el team.
21
+ return `https://app.clickup.com/t/${encodeURIComponent(id)}`;
22
+ }
23
+ if (kind === "jira") {
24
+ const base = String(env.DAI_JIRA_BASE_URL || "").replace(/\/+$/, "");
25
+ return base ? `${base}/browse/${encodeURIComponent(id)}` : null;
26
+ }
27
+ return null; // md: la US es un archivo local, no tiene URL web
28
+ }
29
+
30
+ // La URL final, o null si no hay forma de saberla. `liveUrl` es la que trajo fetchUS.
31
+ export function trackerUrl(id, { env = {}, liveUrl = null } = {}) {
32
+ if (!id) return null;
33
+ const tpl = env.DAI_TRACKER_URL_TEMPLATE;
34
+ if (tpl) return String(tpl).replace(/\{id\}/g, id);
35
+ return liveUrl || deriveTrackerUrl(id, env);
36
+ }
package/docs/PROBAR.md CHANGED
@@ -65,8 +65,9 @@ Si esto anda, el flujo está bien. Pasa al tracker real.
65
65
  cat > .env <<'EOF'
66
66
  DAI_PM=clickup
67
67
  DAI_CLICKUP_TOKEN=pk_XXXXXXXX
68
- DAI_TRACKER_URL_TEMPLATE=https://app.clickup.com/t/{id}
69
68
  EOF
69
+ # El link a la tarea lo deduce dai solo. Solo si tu tracker vive en otra URL:
70
+ # DAI_TRACKER_URL_TEMPLATE=https://mi-tracker/t/{id}
70
71
  dai doctor # confirma DAI_PM=clickup y el token
71
72
 
72
73
  dai link-us 86cxyz # trae la US de ClickUp → branch + implements.yaml
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dforce2055/dai",
3
- "version": "0.8.1",
3
+ "version": "0.8.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/",
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: dai-review
3
- description: "Revisa una Pull/Merge Request de un repo remoto (GitHub o GitLab) de forma consciente de la metodología dai, y deja un comentario estándar en español con errores y mejoras. Corre `dai check` (¿la US está atrasada?), valida el Definition of Done, hace el review de código (correctitud + calidad), compone el comentario estándar y lo postea — vía el MCP del forge si está disponible, o vía `dai forge comment` (token) si no. Invocar como /dai-review <URL-de-la-PR o número>. Usar en el paso 6 de SCRUM-CON-IA (code review), antes de que un partner humano firme."
3
+ description: "Revisa una Pull/Merge Request de un repo remoto (GitHub o GitLab) de forma consciente de la metodología dai, y deja un comentario estándar en español con errores y mejoras. Corre `dai check` (¿la US está atrasada?), valida el Definition of Done, hace el review de código (correctitud + calidad), compone el comentario estándar, TE LO MUESTRA Y ESPERA TU OK, y recién entonces lo postea — vía el MCP del forge si está disponible, o vía `dai forge comment` (token) si no. Nunca postea sin aprobación explícita: sale con tu nombre y tu token. Invocar como /dai-review <URL-de-la-PR o número>. Usar en el paso 6 de SCRUM-CON-IA (code review), antes de que un partner humano firme."
4
4
  ---
5
5
 
6
6
  # dai-review — review de PR consciente de la metodología
@@ -39,7 +39,11 @@ Mismo comentario estándar, dos formas de dejarlo (elige la disponible, en este
39
39
  - 🟡 **Mejoras** de calidad (reuso, simplicidad, eficiencia).
40
40
  - ✅ Lo que está **bien** (refuerza lo bueno).
41
41
  5. **Componer el comentario estándar** (ver formato abajo).
42
- 6. **Postear** por MCP o `dai forge comment`. Confirmar el link al comentario.
42
+ 6. **Mostrarlo y esperar el OK.** Imprimí el comentario **entero**, tal cual va a salir,
43
+ y preguntá: _"¿lo posteo así, lo edito, o lo descarto?"_ **Frená ahí.** Si te piden
44
+ cambios, aplicalos y volvé a mostrarlo. No hay atajo: sin un "sí" explícito en este
45
+ turno, no se postea. Un "sí" de una PR anterior no cuenta para esta.
46
+ 7. **Postear** por MCP o `dai forge comment`. Confirmar el link al comentario.
43
47
 
44
48
  ## El comentario estándar
45
49
 
@@ -64,12 +68,20 @@ Es el mismo que emite `renderReviewComment` del CLI — respeta esta forma:
64
68
  _Revisión asistida por dai. La aprobación la firma un humano (Art. 5 del manifiesto)._
65
69
  ```
66
70
 
67
- ## Dos cortes duros
71
+ ## Tres cortes duros
68
72
 
69
73
  1. **No aprobar.** La skill **comenta**, no firma la aprobación. Eso es de un humano.
70
- 2. **Hallazgos concretos.** Nada de "mejorar la calidad" en abstracto: archivo, línea,
74
+ 2. **No postear sin OK.** El comentario sale **con el token del humano y con su nombre**
75
+ (`GITHUB_TOKEN`/`GITLAB_TOKEN` son suyos): en la PR de un compañero figura como si lo
76
+ hubiera escrito él. Publicar un juicio sobre el código de otro, firmado por alguien
77
+ que no lo leyó, es tan grave como aprobar sin mirar. El paso 6 no es opcional.
78
+ 3. **Hallazgos concretos.** Nada de "mejorar la calidad" en abstracto: archivo, línea,
71
79
  y el porqué. Si no es accionable, no va.
72
80
 
81
+ > **Por qué el corte 2 existe:** esta skill posteaba directo. El Art. 5 estaba bien
82
+ > leído en la letra —no clickeaba Approve— y mal puesto en la práctica: te dejaba
83
+ > firmar en público un review que nunca viste. Que salga bueno era suerte, no diseño.
84
+
73
85
  ## Relación con el modelo
74
86
 
75
87
  - Es el paso 6 de [`SCRUM-CON-IA.md`](../../docs/SCRUM-CON-IA.md).
@@ -42,7 +42,11 @@
42
42
 
43
43
  ## Enlaces relacionados
44
44
 
45
- <!-- US en el tracker, commit ancla, docs, issues. -->
45
+ <!--
46
+ La US, la branch y el commit ancla los precarga `dai pr` en el bloque `dai:links`
47
+ de abajo: NO los escribas a mano ni reescribas ese bloque (se regenera y te lo pisa).
48
+ Acá abajo sumá solo lo que dai no sabe: docs, issues, PRs relacionadas, dependencias.
49
+ -->
46
50
 
47
51
  ---
48
52