@dforce2055/dai 0.8.0 → 0.8.1
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 +68 -0
- package/README.md +33 -8
- package/VERSION +1 -1
- package/cli/dai.mjs +53 -23
- package/cli/lib/http.mjs +7 -1
- package/cli/lib/pm-jira.mjs +29 -10
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,74 @@
|
|
|
3
3
|
Formato basado en [Keep a Changelog](https://keepachangelog.com/). Versionado semver
|
|
4
4
|
(ver `VERSION`).
|
|
5
5
|
|
|
6
|
+
## [0.8.1] — 2026-07-16
|
|
7
|
+
|
|
8
|
+
**Primera prueba real en Windows con analistas y devs de una empresa: el ciclo completo
|
|
9
|
+
contra un Jira corporativo anduvo, y de paso destapó lo que faltaba pulir para que un
|
|
10
|
+
equipo Windows + Copilot + GitLab no se topara con muros.**
|
|
11
|
+
|
|
12
|
+
### Arreglado
|
|
13
|
+
- **`dai sync` crasheaba en repos Copilot** con `ReferenceError: skillToPrompt is not
|
|
14
|
+
defined`. El pase a Agent Skills nativas (0.8.0, [ADR-0014](docs/adr/0014-copilot-agent-skills.md))
|
|
15
|
+
borró `skillToPrompt` y migró `dai init` a `.github/skills/`, pero dejó dos llamadas
|
|
16
|
+
colgadas en el path viejo `.github/prompts/`: `dai sync` y `dai skills install --from`
|
|
17
|
+
(rama Copilot). Ahora ambos copian la skill nativa (`.github/skills/<name>`, con
|
|
18
|
+
`templates/`) igual que `init`, y `sync` **migra** el repo: limpia los `.prompt.md`
|
|
19
|
+
viejos de dai. `dai sync` también detecta Copilot por `.github/skills` (antes solo por
|
|
20
|
+
`.github/prompts`, así que ni veía los repos nuevos). Cubierto por un test de
|
|
21
|
+
integración de `dai sync --for copilot` — el hueco que dejó pasar la regresión.
|
|
22
|
+
- **`dai upgrade` (y `dai init --openspec`) fallaban en Windows** con un genérico *"no
|
|
23
|
+
pude consultar el registry (¿sin red?)"* aunque npm anduviera perfecto. En Windows `npm`
|
|
24
|
+
y `openspec` son shims `.cmd`, y desde Node 18.20 / 20.12 / 21.7 (fix de CVE-2024-27980)
|
|
25
|
+
`execFileSync` **se niega a lanzar un `.cmd` sin `shell:true`** — tira `EINVAL`, que dai
|
|
26
|
+
confundía con falta de red. Ahora esos binarios se corren con `shell` en Windows.
|
|
27
|
+
- **`dai pr` fallaba el push la primera vez contra un remoto HTTPS corporativo.** El push
|
|
28
|
+
corría con stdin ignorado, así que Git Credential Manager no podía pedir la credencial y
|
|
29
|
+
el push moría en seco — mientras el error real de git quedaba oculto tras un genérico
|
|
30
|
+
*"Command failed"*. Ahora el push hereda stdin (con `GIT_TERMINAL_PROMPT=1`) para que el
|
|
31
|
+
login se pueda completar, **muestra el stderr real de git**, y si aún falla sugiere
|
|
32
|
+
pushear a mano una vez para cachear la credencial.
|
|
33
|
+
|
|
34
|
+
### Agregado
|
|
35
|
+
- **`dai mr` como alias de `dai pr`.** En un shop de GitLab uno tipea "mr" (merge request);
|
|
36
|
+
ahora funciona. Es el mismo comando (detecta el forge y usa `gh`/`glab`), solo más natural.
|
|
37
|
+
|
|
38
|
+
### Cambiado
|
|
39
|
+
- **El diagnóstico de un 400 de `dai publish` ahora coincide con lo que Jira dijo.** Antes
|
|
40
|
+
mandaba SIEMPRE a declarar un campo propio (`customfield_NNNNN` en `.dai/jira-fields.json`),
|
|
41
|
+
aunque el 400 fuera *"Por favor indicar épica…"* — confuso, porque el fix real es
|
|
42
|
+
`--parent`. Ahora la ayuda se elige según el cuerpo del error: regla de épica padre →
|
|
43
|
+
`--parent`, campo obligatorio → `jira-fields.json`, genérico → nombra las dos causas sin
|
|
44
|
+
empujar una sola.
|
|
45
|
+
- **`dai pr` explica por qué no pudo crear la PR/MR** en vez de un *"¿instalado y
|
|
46
|
+
autenticado?"*. Si `gh`/`glab` no está instalado (`ENOENT`) lo dice y enlaza la
|
|
47
|
+
instalación + `… auth login` (con `--hostname` para GitLab self-hosted); si falla por
|
|
48
|
+
otra cosa, **imprime el stderr real del forge** (auth vencida, host sin configurar, flag
|
|
49
|
+
desconocido). Era el caso ciego del equipo con GitLab corporativo.
|
|
50
|
+
- **El diagnóstico de TLS ofrece primero el camino más simple**: `NODE_OPTIONS=--use-system-ca`
|
|
51
|
+
(Node ≥ 22.15), que usa el trust store del sistema donde el navegador ya confía en la CA
|
|
52
|
+
de la empresa — validado contra el proxy real. `NODE_EXTRA_CA_CERTS` queda como
|
|
53
|
+
alternativa para cualquier versión de Node. (Sigue prohibido `NODE_TLS_REJECT_UNAUTHORIZED=0`.)
|
|
54
|
+
|
|
55
|
+
### Docs
|
|
56
|
+
- **El prerequisito de `dai pr`: `gh`/`glab` instalado y autenticado.** Un callout en la
|
|
57
|
+
sección del dev con el setup one-time del CLI del forge (`gh auth login` /
|
|
58
|
+
`glab auth login --hostname <tu-gitlab>`, con la nota del PATH en Windows tras instalar con
|
|
59
|
+
winget). Era el paso que faltaba documentar — sin el CLI, `dai pr` pushea igual y avisa.
|
|
60
|
+
- **README consistente con Copilot nativo.** La tabla de superficies ya decía que Copilot
|
|
61
|
+
carga skills en app / CLI / IDE / cloud (solo el chat de github.com queda afuera), pero el
|
|
62
|
+
resto del README seguía en el modelo viejo: la tabla de `--for`, el árbol del repo y el
|
|
63
|
+
snippet de `dai skills install` hablaban de `.github/prompts/*.prompt.md` y de que "Copilot
|
|
64
|
+
no tiene skills instalables". Corregido a `.github/skills/` nativo — validado con un dev
|
|
65
|
+
corriendo dai en la app de GitHub Copilot desktop.
|
|
66
|
+
|
|
67
|
+
### Interno
|
|
68
|
+
- **161 tests** (+3): integración de `dai sync --for copilot` (regresión + migración del
|
|
69
|
+
layout viejo).
|
|
70
|
+
- **Conocido, sin arreglar:** en Windows, tras un `dai publish` con red, puede aparecer al
|
|
71
|
+
salir `Assertion failed: !(handle->flags & UV_HANDLE_CLOSING), src\win\async.c` — un
|
|
72
|
+
crash de *teardown* de libuv, posterior a toda la salida útil (cosmético). Se investiga.
|
|
73
|
+
|
|
6
74
|
## [0.8.0] — 2026-07-15
|
|
7
75
|
|
|
8
76
|
**El primer analista funcional real usó dai contra un Jira corporativo, y encontró el
|
package/README.md
CHANGED
|
@@ -49,7 +49,7 @@ deja las skills disponibles:
|
|
|
49
49
|
|
|
50
50
|
```bash
|
|
51
51
|
npm i -g @dforce2055/dai # el CLI
|
|
52
|
-
dai skills install # skills de IA → Claude y Cursor (global por defecto)
|
|
52
|
+
dai skills install # skills de IA → Claude, Copilot y Cursor (global por defecto)
|
|
53
53
|
```
|
|
54
54
|
|
|
55
55
|
Y después, en el chat del asistente, según lo que tengas:
|
|
@@ -66,6 +66,20 @@ Y después, en el chat del asistente, según lo que tengas:
|
|
|
66
66
|
> (`DAI_PM` en el `.env`): por el **MCP** de Jira/ClickUp si está conectado, o con
|
|
67
67
|
> **`dai publish <us.md>`** si no (crea el issue vía token). Tú respondes y decides.
|
|
68
68
|
|
|
69
|
+
¿Sin MCP, o querés publicar a mano? `dai publish` sube el `.md` al tracker y te devuelve el key:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
dai publish historia.md # crea la US → devuelve el key
|
|
73
|
+
dai publish historia.md --parent PROJ-42 # colgándola de su épica
|
|
74
|
+
dai publish historia.md --field clasificacion=Mejora # con un campo propio que tu Jira exige
|
|
75
|
+
dai publish epica.md --issuetype Epic # publicar una épica (no una US)
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
> **Jira corporativo:** si tu proyecto exige campos propios (`customfield_…`), declaralos una
|
|
79
|
+
> vez en `.dai/jira-fields.json` (molde en `.dai/templates/`) y pasalos con `--field`. Si una
|
|
80
|
+
> historia tiene que colgar de una épica, `--parent PROJ-42`. dai valida antes de la red y, si
|
|
81
|
+
> Jira igual rechaza, el error te dice **qué** falta ([ADR-0015](docs/adr/0015-jira-corporativo.md)).
|
|
82
|
+
|
|
69
83
|
### 🔵 Como dev — tengo una US y voy a implementarla
|
|
70
84
|
|
|
71
85
|
Una vez, preparas el repo:
|
|
@@ -98,6 +112,17 @@ dai pr --assignee <compañero> # crea la PR precargada y se la asigna a un com
|
|
|
98
112
|
dai stamp # al mergear: estampa la cobertura en el tracker
|
|
99
113
|
```
|
|
100
114
|
|
|
115
|
+
> **Para que `dai pr` cree la PR/MR** necesitás el CLI del forge instalado y autenticado, una
|
|
116
|
+
> sola vez por máquina:
|
|
117
|
+
> - **GitHub** → [`gh`](https://cli.github.com) · `gh auth login`
|
|
118
|
+
> - **GitLab** → [`glab`](https://gitlab.com/gitlab-org/cli) · `glab auth login --hostname <tu-gitlab>`
|
|
119
|
+
> — en un GitLab self-hosted/corporativo el `--hostname` es **obligatorio**, y el token va con
|
|
120
|
+
> scope `api`. (En Windows, tras instalar `glab` con `winget`, abrí una terminal nueva para que
|
|
121
|
+
> tome el PATH.)
|
|
122
|
+
>
|
|
123
|
+
> Sin el CLI del forge, `dai pr` **igual pushea tu branch** y te dice qué instalar — después
|
|
124
|
+
> creás la PR/MR a mano desde la web. dai nunca te deja a medias sin decir por qué.
|
|
125
|
+
|
|
101
126
|
## Cómo usarlo — el flujo, paso a paso
|
|
102
127
|
|
|
103
128
|
Guía completa con salidas reales en [`docs/PROBAR.md`](docs/PROBAR.md); un caso narrado en
|
|
@@ -157,16 +182,16 @@ flowchart TD
|
|
|
157
182
|
| Comando | Qué hace |
|
|
158
183
|
|---|---|
|
|
159
184
|
| `dai init [<repo>]` | scaffolder interactivo del repo. Flags: `--for claude\|copilot\|both\|cursor\|all` (asistente, default `all`) · `--pm md\|jira\|clickup` (tracker) · `--openspec` |
|
|
160
|
-
| `dai skills install [--global \| --local <repo>] [--force] [--dry-run] [--for claude\|cursor\|all]` | instala/actualiza las skills de dai en Claude y/o Cursor (`--for all` por defecto). `--force` re-copia. Alias: **`dai install`**. Ej: `dai skills install --local . --for cursor --force` |
|
|
185
|
+
| `dai skills install [--global \| --local <repo>] [--force] [--dry-run] [--for claude\|copilot\|cursor\|all]` | instala/actualiza las skills de dai en Claude, Copilot y/o Cursor (`--for all` por defecto; Copilot global → `~/.copilot/skills`, local → `.github/skills/`). `--force` re-copia. Alias: **`dai install`**. Ej: `dai skills install --local . --for cursor --force` |
|
|
161
186
|
| `dai skills install --from <git-url\|path>[#ref] [--for …]` | instala **skills externas** (por-stack: .NET, Java, …) desde un repo/dir, **convertidas para los 3 asistentes**. Self-service, one-off, sin registro; `dai sync` no las toca. Colisión con una skill de dai → salta ([ADR-0013](docs/adr/0013-skills-externas-install-from.md)). Ej: `dai skills install --from github.com/mi-org/net-skills` |
|
|
162
187
|
| `dai sync [--dry-run] [--for <asistentes>]` | **refresca** skills, constitución, templates y PR template a la versión del CLI — **aditivo** (no pisa tu `CLAUDE.md`), no toca el `.env` ni OpenSpec. Detecta los asistentes del repo o pasás `--for`. `--dry-run` muestra qué cambiaría ([ADR-0010](docs/adr/0010-versionado-y-upgrade.md)) |
|
|
163
188
|
| `dai upgrade [--check] [--dry-run]` · alias `dai update` | **actualiza el CLI global** a la última publicada (`npm i -g …@latest`) — self-update. **No toca el repo**: reporta el drift del scaffold pero deja el `dai sync` al mantenedor. `--check` solo informa · `--dry-run` muestra el comando ([ADR-0012](docs/adr/0012-upgrade-self-update-del-cli.md)) |
|
|
164
|
-
| `dai publish <us.md
|
|
189
|
+
| `dai publish <us.md> [--parent KEY] [--issuetype T] [--field alias=valor]` | crea la US en el tracker (Jira/ClickUp/md) desde un `.md` y devuelve el key — el fallback del MCP para publicar sin el asistente. `--parent PROJ-42` la cuelga de su épica · `--issuetype Epic` publica una épica (fallback CLI de `grill-epic`) · `--field clasificacion=Mejora` (repetible) manda los campos propios que exige tu Jira, declarados en `.dai/jira-fields.json` ([ADR-0015](docs/adr/0015-jira-corporativo.md)) |
|
|
165
190
|
| `dai link-us <ID> [--us <md>]` | crea branch + `implements.yaml`; sin `--us` trae la US del tracker |
|
|
166
191
|
| `dai link-us <ID> --resync` | re-estampa el `ac_hash` contra la US viva (tras un ⚠️ de check) |
|
|
167
192
|
| `dai check` | compara tu código vs la US viva → ✅ al día / ⚠️ atrasado (exit code = gate de PR) |
|
|
168
193
|
| `dai ls [--json]` | lista las US que implementa el repo + su link al tracker |
|
|
169
|
-
| `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 |
|
|
194
|
+
| `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 |
|
|
170
195
|
| `dai stamp` | estampa la cobertura inversa en el tracker (branch + commit-ancla) |
|
|
171
196
|
| `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 |
|
|
172
197
|
| `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` |
|
|
@@ -214,14 +239,14 @@ ver [`.env.example`](.env.example). Auth (SSH + tokens): [ADR-0007](docs/adr/000
|
|
|
214
239
|
| Valor | Genera | Elígelo si… |
|
|
215
240
|
|---|---|---|
|
|
216
241
|
| `--for claude` | `.claude/skills/` + `CLAUDE.md` | tu equipo usa **Claude** (Code / Desktop) |
|
|
217
|
-
| `--for copilot` | `.github/
|
|
242
|
+
| `--for copilot` | `.github/skills/` + `.github/copilot-instructions.md` | tu equipo usa **GitHub Copilot** (app, CLI, VS Code / JetBrains o cloud agent) |
|
|
218
243
|
| `--for cursor` | `.cursor/skills/` + `.cursor/rules/dai-constitution.mdc` | tu equipo usa **Cursor Agent** |
|
|
219
244
|
| `--for both` | Claude + Copilot | equipo mixto sin Cursor |
|
|
220
245
|
| `--for all` *(default)* | Claude + Copilot + Cursor | quieres dejar el repo listo para cualquier asistente |
|
|
221
246
|
|
|
222
247
|
- **Combinables:** pasá un subconjunto separado por coma — `--for claude,cursor` o
|
|
223
|
-
`--for copilot`. `all` = los tres · `both` = Claude+Copilot. (Igual en `dai install
|
|
224
|
-
|
|
248
|
+
`--for copilot`. `all` = los tres · `both` = Claude+Copilot. (Igual en `dai install`:
|
|
249
|
+
Copilot lee `SKILL.md` nativo, así que sus skills se instalan como las de Claude/Cursor.)
|
|
225
250
|
- Es **aditivo, no destructivo**: los conjuntos conviven sin pisarse (viven en
|
|
226
251
|
carpetas distintas). La **misma** skill se transforma al formato de cada asistente.
|
|
227
252
|
- Si ejecutas `dai init` sin flag, te lo pregunta de forma interactiva.
|
|
@@ -241,7 +266,7 @@ mi-repo/
|
|
|
241
266
|
│ └── doc-to-backlog · grill-intent · grill-epic · grill-user-story · link-us · tdd · dai-review
|
|
242
267
|
├── .github/
|
|
243
268
|
│ ├── copilot-instructions.md · La constitución, auto-inyectada en cada chat de Copilot
|
|
244
|
-
│ ├──
|
|
269
|
+
│ ├── skills/ · Las mismas skills, en formato Copilot nativo (SKILL.md)
|
|
245
270
|
│ └── pull_request_template.md · Molde de PR/MR atado al link
|
|
246
271
|
├── .cursor/
|
|
247
272
|
│ ├── skills/ · Las mismas skills, en formato Cursor
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.8.
|
|
1
|
+
0.8.1
|
package/cli/dai.mjs
CHANGED
|
@@ -58,9 +58,14 @@ function git(args, opts = {}) {
|
|
|
58
58
|
// quedan en e.stderr para quien quiera inspeccionarlos.
|
|
59
59
|
return execFileSync("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], ...opts }).trim();
|
|
60
60
|
}
|
|
61
|
-
// En Windows los binarios instalados por npm (npm, openspec) son shims `.cmd
|
|
62
|
-
//
|
|
63
|
-
|
|
61
|
+
// En Windows los binarios instalados por npm (npm, openspec) son shims `.cmd`. Desde
|
|
62
|
+
// Node 18.20 / 20.12 / 21.7 (fix de CVE-2024-27980) execFileSync se NIEGA a lanzar un
|
|
63
|
+
// `.cmd` salvo con shell:true — si no, tira EINVAL, que dai confundía con "sin red".
|
|
64
|
+
// Por eso van con shell en Windows. git/gh/glab son `.exe`: se resuelven normal, sin shell.
|
|
65
|
+
function runNpmTool(name, args, opts = {}) {
|
|
66
|
+
const win = process.platform === "win32";
|
|
67
|
+
return execFileSync(win ? `${name}.cmd` : name, args, { shell: win, ...opts });
|
|
68
|
+
}
|
|
64
69
|
function trackerUrl(id) {
|
|
65
70
|
const tpl = process.env.DAI_TRACKER_URL_TEMPLATE;
|
|
66
71
|
return tpl ? tpl.replace("{id}", id) : id;
|
|
@@ -423,8 +428,16 @@ async function cmdPr(opts) {
|
|
|
423
428
|
writeFileSync(bodyFile, body);
|
|
424
429
|
try {
|
|
425
430
|
info(`Publicando la branch ${branch}…`);
|
|
426
|
-
|
|
427
|
-
|
|
431
|
+
// stdin heredado + GIT_TERMINAL_PROMPT=1: la PRIMERA vez contra un remoto HTTPS
|
|
432
|
+
// corporativo, git/credential-manager necesita poder pedir la credencial. Con stdin
|
|
433
|
+
// ignorado (el default de git()) el login no completaba y el push fallaba en seco.
|
|
434
|
+
git(["push", "-u", "origin", branch], { stdio: ["inherit", "pipe", "pipe"], env: { ...process.env, GIT_TERMINAL_PROMPT: "1" } });
|
|
435
|
+
} catch (e) {
|
|
436
|
+
const err = String(e.stderr || e.message || "").trim();
|
|
437
|
+
if (err) process.stderr.write(" " + err.split("\n").join("\n ") + "\n");
|
|
438
|
+
process.stdout.write(` Si es la primera vez contra este remoto, autenticá pusheando a mano una vez:\n git push -u origin ${branch}\n y volvé a correr: dai pr\n`);
|
|
439
|
+
fail(`no pude pushear la branch '${branch}'.`, 1);
|
|
440
|
+
}
|
|
428
441
|
|
|
429
442
|
const cmd = tool === "gh"
|
|
430
443
|
? ["pr", "create", "--title", title, "--body-file", bodyFile, "--base", base,
|
|
@@ -439,12 +452,20 @@ async function cmdPr(opts) {
|
|
|
439
452
|
try { rmSync(bodyFile); } catch { /* noop */ }
|
|
440
453
|
} catch (e) {
|
|
441
454
|
const msg = String(e.stderr || e.message || "");
|
|
442
|
-
|
|
455
|
+
const manual = ` Comando listo para correr a mano:\n ${tool} ${cmd.map((c) => /\s/.test(c) ? `'${c}'` : c).join(" ")}\n`;
|
|
456
|
+
if (e.code === "ENOENT") {
|
|
457
|
+
// El binario del forge no está instalado (el caso más común detrás de "no salió la MR").
|
|
458
|
+
const doc = tool === "glab" ? "https://gitlab.com/gitlab-org/cli/-/releases" : "https://cli.github.com";
|
|
459
|
+
warn(`'${tool}' no está instalado. El body quedó en ${bodyFile}.`);
|
|
460
|
+
process.stdout.write(` Instalá ${tool} (${doc}) y autenticá con \`${tool} auth login\`${tool === "glab" ? " --hostname " + (parseRemote(remote)?.host || "tu-gitlab") : ""}, o creá la PR a mano.\n${manual}`);
|
|
461
|
+
} else if (/no history in common|no commits between|not found.*base|base.*not found/i.test(msg)) {
|
|
443
462
|
warn(`la branch no comparte historia con '${base}' en el remoto (o '${base}' no existe allá).`);
|
|
444
463
|
process.stdout.write(` Suele pasar cuando el repo local y el remoto son distintos. Empuja la base primero:\n git push origin ${base}\n y vuelve a correr: dai pr\n`);
|
|
445
464
|
} else {
|
|
446
|
-
warn(`no pude crear la PR con ${tool}
|
|
447
|
-
|
|
465
|
+
warn(`no pude crear la PR con ${tool}. El body quedó en ${bodyFile}.`);
|
|
466
|
+
// El stderr real de gh/glab (auth vencida, host no configurado, flag desconocido…).
|
|
467
|
+
if (msg.trim()) process.stdout.write(` ${tool} dijo:\n ${msg.trim().split("\n").join("\n ")}\n`);
|
|
468
|
+
process.stdout.write(manual);
|
|
448
469
|
}
|
|
449
470
|
}
|
|
450
471
|
}
|
|
@@ -595,8 +616,9 @@ function cmdInstallFrom(opts) {
|
|
|
595
616
|
rmSync(t, { recursive: true, force: true }); cpSync(srcDir, t, { recursive: true }); writeFileSync(join(t, "SKILL.md"), skillToCursor(md));
|
|
596
617
|
}
|
|
597
618
|
if (want.copilot) {
|
|
598
|
-
|
|
599
|
-
|
|
619
|
+
// Copilot lee SKILL.md nativo (Agent Skills, ADR-0014): copia cruda, con templates/.
|
|
620
|
+
const t = join(repo, ".github", "skills", name);
|
|
621
|
+
rmSync(t, { recursive: true, force: true }); mkdirSync(dirname(t), { recursive: true }); cpSync(srcDir, t, { recursive: true });
|
|
600
622
|
}
|
|
601
623
|
ok(name); installed++;
|
|
602
624
|
}
|
|
@@ -763,7 +785,7 @@ async function cmdInit(repo, opts) {
|
|
|
763
785
|
// así que sí lo inicializamos nosotros — mapeando --for a sus tools. (Antes se
|
|
764
786
|
// dejaba a medias porque se intentaba correr su modo interactivo anidado.)
|
|
765
787
|
process.stdout.write("\n");
|
|
766
|
-
const openspecPresent = () => { try {
|
|
788
|
+
const openspecPresent = () => { try { runNpmTool("openspec", ["--version"], { stdio: "ignore" }); return true; } catch { return false; } };
|
|
767
789
|
const osTools = [want.claude && "claude", want.copilot && "github-copilot", want.cursor && "cursor"]
|
|
768
790
|
.filter(Boolean).join(",") || "claude,github-copilot,cursor";
|
|
769
791
|
const osHint = "para sumarlo después: npm i -g @fission-ai/openspec@latest && openspec init --tools " + osTools;
|
|
@@ -774,13 +796,13 @@ async function cmdInit(repo, opts) {
|
|
|
774
796
|
} else if (installOpenspec) {
|
|
775
797
|
let cliOk = openspecPresent();
|
|
776
798
|
if (!cliOk) {
|
|
777
|
-
try { info("OpenSpec: instalando el CLI (npm i -g @fission-ai/openspec)…");
|
|
799
|
+
try { info("OpenSpec: instalando el CLI (npm i -g @fission-ai/openspec)…"); runNpmTool("npm", ["install", "-g", "@fission-ai/openspec@latest"], { stdio: "inherit" }); cliOk = openspecPresent(); }
|
|
778
800
|
catch { cliOk = false; }
|
|
779
801
|
}
|
|
780
802
|
if (cliOk) {
|
|
781
803
|
try {
|
|
782
804
|
info(`OpenSpec: inicializando en el repo (--tools ${osTools})…`);
|
|
783
|
-
|
|
805
|
+
runNpmTool("openspec", ["init", "--tools", osTools, "--force"], { stdio: "inherit", cwd: repo === "." ? process.cwd() : repo });
|
|
784
806
|
ok("OpenSpec: instalado e inicializado — genera design/tasks con /opsx:*");
|
|
785
807
|
} catch {
|
|
786
808
|
warn("OpenSpec: el CLI está pero falló `openspec init`. Ejecuta a mano en el repo:");
|
|
@@ -836,7 +858,7 @@ function cmdArchive(changeArg, opts) {
|
|
|
836
858
|
const args = ["archive", change, "--yes"];
|
|
837
859
|
if (opts.skipSpecs) args.push("--skip-specs");
|
|
838
860
|
try {
|
|
839
|
-
|
|
861
|
+
runNpmTool("openspec", args, { stdio: "inherit", cwd: repo });
|
|
840
862
|
} catch (e) {
|
|
841
863
|
fail(`openspec archive falló (¿tasks incompletas? ¿change inexistente?): ${String(e.message).split("\n")[0]}`, 1);
|
|
842
864
|
}
|
|
@@ -861,11 +883,13 @@ function cmdSync(repo, opts) {
|
|
|
861
883
|
} else {
|
|
862
884
|
want = {
|
|
863
885
|
claude: existsSync(join(repo, ".claude", "skills")),
|
|
864
|
-
|
|
886
|
+
// .github/skills es el layout nativo (ADR-0014); .github/prompts es el viejo, que
|
|
887
|
+
// seguimos detectando para migrar repos que quedaron en el formato anterior.
|
|
888
|
+
copilot: existsSync(join(repo, ".github", "skills")) || existsSync(join(repo, ".github", "prompts")),
|
|
865
889
|
cursor: existsSync(join(repo, ".cursor", "skills")),
|
|
866
890
|
};
|
|
867
891
|
if (!want.claude && !want.copilot && !want.cursor)
|
|
868
|
-
fail("no detecté asistentes instalados (.claude
|
|
892
|
+
fail("no detecté asistentes instalados (.claude/skills · .github/skills · .cursor/skills). Pasá --for.", 2);
|
|
869
893
|
}
|
|
870
894
|
|
|
871
895
|
const cliV = readFileSync(join(ROOT, "VERSION"), "utf8").trim();
|
|
@@ -900,11 +924,16 @@ function cmdSync(repo, opts) {
|
|
|
900
924
|
const cPath = join(repo, "CLAUDE.md"), cCur = existsSync(cPath) ? readFileSync(cPath, "utf8") : "";
|
|
901
925
|
writeFileSync(cPath, upsertBlock(cCur, constitution("claude")));
|
|
902
926
|
});
|
|
903
|
-
if (want.copilot) step(`Copilot: .github/
|
|
904
|
-
|
|
905
|
-
|
|
927
|
+
if (want.copilot) step(`Copilot: .github/skills/ (${skills.length}) + copilot-instructions.md`, () => {
|
|
928
|
+
// Copilot lee SKILL.md nativo (ADR-0014): copia cruda, con templates/ — igual que `dai init`.
|
|
929
|
+
const dir = join(repo, ".github", "skills"); mkdirSync(dir, { recursive: true });
|
|
930
|
+
for (const name of skills) cpSync(join(skillsSrc, name), join(dir, name), { recursive: true });
|
|
906
931
|
const ciPath = join(repo, ".github", "copilot-instructions.md"), ciCur = existsSync(ciPath) ? readFileSync(ciPath, "utf8") : "";
|
|
907
932
|
writeFileSync(ciPath, upsertBlock(ciCur, constitution("copilot")));
|
|
933
|
+
// Limpia los .prompt.md viejos de dai (ahora son skills nativas) para no duplicar cada /comando.
|
|
934
|
+
const pdir = join(repo, ".github", "prompts");
|
|
935
|
+
const stale = existsSync(pdir) ? stalePromptFiles(skills).filter((f) => existsSync(join(pdir, f))) : [];
|
|
936
|
+
for (const f of stale) rmSync(join(pdir, f), { force: true });
|
|
908
937
|
});
|
|
909
938
|
if (want.cursor) step(`Cursor: .cursor/skills/ (${skills.length}) + .cursor/rules/dai-constitution.mdc`, () => {
|
|
910
939
|
const dir = join(repo, ".cursor", "skills"); mkdirSync(dir, { recursive: true });
|
|
@@ -963,7 +992,7 @@ function cmdUpgrade(opts) {
|
|
|
963
992
|
|
|
964
993
|
let latestV;
|
|
965
994
|
try {
|
|
966
|
-
latestV =
|
|
995
|
+
latestV = runNpmTool("npm", ["view", name, "version"], { encoding: "utf8" }).trim();
|
|
967
996
|
} catch {
|
|
968
997
|
fail(`no pude consultar el registry (¿sin red?). Actualizá a mano: ${manual}`, 1);
|
|
969
998
|
}
|
|
@@ -979,7 +1008,7 @@ function cmdUpgrade(opts) {
|
|
|
979
1008
|
|
|
980
1009
|
info(`actualizando v${plan.from} → v${plan.to} …`);
|
|
981
1010
|
try {
|
|
982
|
-
|
|
1011
|
+
runNpmTool("npm", ["install", "-g", `${name}@latest`], { stdio: "inherit" });
|
|
983
1012
|
} catch {
|
|
984
1013
|
fail(`el install falló. Probá a mano: ${manual}`, 1);
|
|
985
1014
|
}
|
|
@@ -1093,7 +1122,8 @@ switch (cmd) {
|
|
|
1093
1122
|
case "stamp": cmdStamp().catch((e) => fail(String(e.message))); break;
|
|
1094
1123
|
case "forge": cmdForge(pos[0], pos[1], opts).catch((e) => fail(String(e.message))); break;
|
|
1095
1124
|
case "publish": cmdPublish(pos[0], opts).catch((e) => fail(String(e.message))); break;
|
|
1096
|
-
case "pr":
|
|
1125
|
+
case "pr":
|
|
1126
|
+
case "mr": cmdPr(opts).catch((e) => fail(String(e.message))); break; // `mr` = alias para GitLab (merge request)
|
|
1097
1127
|
case "done": cmdDone(opts); break;
|
|
1098
1128
|
case "archive": cmdArchive(pos[0], opts); break;
|
|
1099
1129
|
case "install": cmdInstall(opts).catch((e) => fail(String(e.message))); break; // alias de `dai skills install`
|
|
@@ -1123,7 +1153,7 @@ switch (cmd) {
|
|
|
1123
1153
|
" stamp estampa la cobertura en el tracker (ADR-0005)\n" +
|
|
1124
1154
|
" done [--base main] [--force] cierra la US: vuelve a la base, actualiza y borra la branch local (si está mergeada)\n" +
|
|
1125
1155
|
" 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" +
|
|
1126
|
-
" pr [--assignee u] [--base b] [--draft] [--yes] crea TU PR/MR precargada (muestra + confirma)\n" +
|
|
1156
|
+
" pr (alias mr) [--assignee u] [--base b] [--draft] [--yes] crea TU PR/MR precargada (muestra + confirma)\n" +
|
|
1127
1157
|
" forge comment <ref> --body-file <f> · forge pr <ref> comentar/leer una PR ajena (github/gitlab)\n\n" +
|
|
1128
1158
|
"Instalación:\n" +
|
|
1129
1159
|
" skills install [--global | --local <repo>] [--force] [--dry-run] [--for <asistentes>] instala las skills de dai (alias: `install`)\n" +
|
package/cli/lib/http.mjs
CHANGED
|
@@ -35,7 +35,13 @@ export function tlsHint(host, code) {
|
|
|
35
35
|
" Casi siempre es un proxy corporativo que intercepta TLS con su propia CA. Node no",
|
|
36
36
|
" usa el trust store de Windows/macOS, por eso el navegador anda y dai no.",
|
|
37
37
|
"",
|
|
38
|
-
" Solución
|
|
38
|
+
" Solución más simple (Node ≥ 22.15): usá el trust store del sistema, donde tu navegador",
|
|
39
|
+
" ya confía en la CA de tu empresa:",
|
|
40
|
+
" Windows (PowerShell): $env:NODE_OPTIONS=\"--use-system-ca\"",
|
|
41
|
+
" macOS / Linux: export NODE_OPTIONS=--use-system-ca",
|
|
42
|
+
"",
|
|
43
|
+
" Alternativa (cualquier versión de Node): exportá la CA raíz de tu empresa a un .pem",
|
|
44
|
+
" y declarásela a Node:",
|
|
39
45
|
" Windows (PowerShell): $env:NODE_EXTRA_CA_CERTS=\"C:\\ruta\\ca-empresa.pem\"",
|
|
40
46
|
" macOS / Linux: export NODE_EXTRA_CA_CERTS=/ruta/ca-empresa.pem",
|
|
41
47
|
"",
|
package/cli/lib/pm-jira.mjs
CHANGED
|
@@ -106,16 +106,32 @@ export function renderCoverageAdf(id, r) {
|
|
|
106
106
|
return { type: "doc", version: 1, content };
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
-
// Un 400 al crear
|
|
110
|
-
//
|
|
111
|
-
|
|
109
|
+
// Un 400 al crear puede ser (a) un campo propio obligatorio que no mandamos, (b) una regla
|
|
110
|
+
// del proyecto —p. ej. exigir una épica padre— o (c) otra cosa. Jira dice cuál en el cuerpo,
|
|
111
|
+
// pero no dice qué hacer: elegimos la ayuda según lo que dice el error, no una fija. Antes
|
|
112
|
+
// mandábamos SIEMPRE a declarar un campo, aunque el 400 fuera "falta la épica" (confuso).
|
|
113
|
+
export function createHint(status, body = "") {
|
|
112
114
|
if (status !== 400) return "";
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
115
|
+
const b = String(body);
|
|
116
|
+
// Regla de workflow: la historia tiene que colgar de una épica (no es un campo propio).
|
|
117
|
+
if (/[ée]pica|epic|parent/i.test(b)) {
|
|
118
|
+
return "\n\n Tu proyecto pide que esta historia cuelgue de una épica. Pasá la key de la\n" +
|
|
119
|
+
" épica padre con --parent (es la épica, no el proyecto):\n" +
|
|
120
|
+
" dai publish <us.md> --parent PROJ-123 --field clasificacion=<valor>";
|
|
121
|
+
}
|
|
122
|
+
// Campo propio obligatorio que no mandamos (el customfield_NNNNN del error).
|
|
123
|
+
if (/customfield_/i.test(b)) {
|
|
124
|
+
return "\n\n Tu proyecto exige un campo propio (el 'customfield_NNNNN' del error).\n" +
|
|
125
|
+
" Declaralo en .dai/jira-fields.json y volvé a publicar — no hace falta improvisar\n" +
|
|
126
|
+
" una llamada a mano:\n" +
|
|
127
|
+
' { "Story": { "clasificacion": { "field": "customfield_NNNNN",\n' +
|
|
128
|
+
' "options": ["Mejora", "Corrección"] } } }\n' +
|
|
129
|
+
" dai publish <us.md> --field clasificacion=Corrección";
|
|
130
|
+
}
|
|
131
|
+
// 400 genérico: no adivinamos. Nombramos las dos causas típicas sin empujar una.
|
|
132
|
+
return "\n\n Jira rechazó la creación (400) — el mensaje de arriba dice por qué. Las dos causas\n" +
|
|
133
|
+
" típicas: un campo propio obligatorio (declaralo en .dai/jira-fields.json) o una regla\n" +
|
|
134
|
+
" del proyecto como exigir una épica padre (--parent PROJ-123).";
|
|
119
135
|
}
|
|
120
136
|
|
|
121
137
|
export function jiraAdapter(env) {
|
|
@@ -155,7 +171,10 @@ export function jiraAdapter(env) {
|
|
|
155
171
|
method: "POST", headers: jiraAuthHeaders(env),
|
|
156
172
|
body: JSON.stringify({ fields: payload }),
|
|
157
173
|
});
|
|
158
|
-
if (!res.ok)
|
|
174
|
+
if (!res.ok) {
|
|
175
|
+
const body = await res.text();
|
|
176
|
+
throw new Error(`jira ${res.status}: ${body}${createHint(res.status, body)}`);
|
|
177
|
+
}
|
|
159
178
|
const j = await res.json();
|
|
160
179
|
return { id: j.key, url: `${trim(base)}/browse/${j.key}` };
|
|
161
180
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dforce2055/dai",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.1",
|
|
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/",
|