@dforce2055/dai 0.12.0 → 0.13.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 +128 -0
- package/VERSION +1 -1
- package/cli/dai.mjs +126 -25
- package/cli/lib/bootstrap.mjs +43 -2
- package/cli/lib/git-ssh.mjs +107 -0
- package/cli/lib/link-us.mjs +5 -2
- package/docs/EJEMPLO-END-TO-END.md +1 -1
- package/docs/glosario.md +1 -1
- package/docs/guias/dev.md +11 -1
- package/docs/tutoriales/setup-dev.md +114 -14
- package/package.json +1 -1
- package/skills/link-us/SKILL.md +1 -1
- package/skills/link-us/templates/implements.yaml +4 -2
- package/templates/definition-of-done.md +6 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,132 @@
|
|
|
3
3
|
Formato basado en [Keep a Changelog](https://keepachangelog.com/). Versionado semver
|
|
4
4
|
(ver `VERSION`).
|
|
5
5
|
|
|
6
|
+
## [0.13.1] — 2026-08-24
|
|
7
|
+
|
|
8
|
+
**Un dev en Windows no podía pushear contra el GitLab de su empresa. `ssh -T` le autenticaba
|
|
9
|
+
perfecto, `git push` moría con `Permission denied (publickey…)`. Dos días buscando el problema
|
|
10
|
+
en la clave, en el token y en los permisos del server: no estaba en ninguno de los tres, y dai
|
|
11
|
+
tapaba la única línea que lo decía.**
|
|
12
|
+
|
|
13
|
+
Su clave tenía passphrase. En Windows conviven dos `ssh.exe` — el de OpenSSH for Windows, que
|
|
14
|
+
habla con el servicio `ssh-agent`, y el que trae Git for Windows, que no lo ve. `ssh -T` usaba
|
|
15
|
+
el primero (la firma la hacía el agente, passphrase nunca), `git push` usaba el segundo y pedía
|
|
16
|
+
la passphrase por stderr. Ese prompt caía en un pipe de dai: el dev veía un cuelgue sin
|
|
17
|
+
explicación, ssh se rendía, caía a autenticación por password y lo único legible al final era
|
|
18
|
+
un error que acusa a la clave. Todo lo que hacía falta para resolverlo estaba en pantalla, y no
|
|
19
|
+
llegaba.
|
|
20
|
+
|
|
21
|
+
### Arreglado
|
|
22
|
+
- **`dai pr` se tragaba lo que git y ssh preguntan.** El push corría con `stderr` en `pipe`
|
|
23
|
+
para no ensuciar la salida, pero git y ssh **preguntan por stderr**: la passphrase de una
|
|
24
|
+
clave, la confirmación de un host nuevo, el aviso del credential manager. Con el prompt
|
|
25
|
+
invisible el comando no se cuelga por un bug, se cuelga esperando una respuesta que nadie
|
|
26
|
+
sabe que tiene que dar. Ahora `stderr` va heredado y el push pregunta a la vista. El error de
|
|
27
|
+
git se lee en vivo, cuando todavía sirve, en vez de aparecer resumido después del fracaso.
|
|
28
|
+
- **La pista al fallar el push asumía que el remoto era HTTPS.** Decía siempre *"si es la
|
|
29
|
+
primera vez contra este remoto, autenticá pusheando a mano una vez"*. Eso arregla HTTPS,
|
|
30
|
+
donde el credential manager pide la credencial la primera vez. Contra un remoto **SSH** el
|
|
31
|
+
push a mano falla exactamente igual — así que la pista mandaba a repetir un comando condenado
|
|
32
|
+
y a seguir buscando en el lugar equivocado. Ahora el consejo depende del transporte: en SSH
|
|
33
|
+
aclara que el token de `gh`/`glab` no interviene en el push, que lo que hay que mirar es la
|
|
34
|
+
clave, y deriva a `dai doctor`.
|
|
35
|
+
|
|
36
|
+
### Agregado
|
|
37
|
+
- **`dai doctor` — sección `forge`.** Reporta el remoto `origin` con su forge detectado y, en
|
|
38
|
+
Windows con remoto SSH, **qué `ssh.exe` va a usar git**: si es el suyo (el de Git for
|
|
39
|
+
Windows, que no llega al `ssh-agent`), lo advierte, explica el modo de falla y da el fix
|
|
40
|
+
(`git config --global core.sshCommand "C:/Windows/System32/OpenSSH/ssh.exe"`). Respeta la
|
|
41
|
+
precedencia real de git —`GIT_SSH_COMMAND` > `GIT_SSH` > `core.sshCommand`— y avisa cuando
|
|
42
|
+
una variable de entorno está pisando un `core.sshCommand` que ya estaba bien: ese caso es
|
|
43
|
+
particularmente cruel, porque el dev arregla el config, no funciona, y mirando el
|
|
44
|
+
`.gitconfig` no hay nada que ver. Fuera de Windows, o con un remoto HTTPS, no opina.
|
|
45
|
+
Núcleo puro y testeado en `cli/lib/git-ssh.mjs` (14 tests); en `dai.mjs` queda solo el I/O.
|
|
46
|
+
|
|
47
|
+
## [0.13.0] — 2026-08-21
|
|
48
|
+
|
|
49
|
+
**Un dev de backend en Windows siguió el tutorial al pie de la letra y el agente se puso a
|
|
50
|
+
programar sin escribir la propuesta. No era Windows ni era su setup: le estábamos diciendo mal
|
|
51
|
+
el nombre del comando.**
|
|
52
|
+
|
|
53
|
+
### Arreglado
|
|
54
|
+
- **Los comandos de OpenSpec se documentaban solo en la forma de Claude Code.** OpenSpec
|
|
55
|
+
genera un archivo distinto por asistente, y el nombre del comando sale del archivo:
|
|
56
|
+
`.claude/commands/opsx/<id>.md` → `/opsx:propose`, pero
|
|
57
|
+
`.github/prompts/opsx-<id>.prompt.md` → `/opsx-propose` (Copilot) y
|
|
58
|
+
`.cursor/commands/opsx-<id>.md` → `/opsx-propose` (Cursor). O sea: **solo Claude usa los
|
|
59
|
+
dos puntos**, y el tutorial de setup del dev —que es el de Windows + Copilot— mostraba los
|
|
60
|
+
dos puntos en los tres pasos.
|
|
61
|
+
El síntoma no se parece en nada a la causa, y ahí está el daño: tipear `/opsx:propose` en
|
|
62
|
+
Copilot **no da error**. No matchea ningún comando, el workflow nunca se carga, y el agente
|
|
63
|
+
toma el texto suelto como una charla — se saltea el gate propose → aprobación → apply y
|
|
64
|
+
arranca a implementar. Se lee como "el bot hace lo que quiere" y se le echa la culpa al
|
|
65
|
+
asistente, al sistema operativo o al método. Es vibe coding servido por la documentación.
|
|
66
|
+
Ahora `dai init` imprime la forma que le toca a **tu** asistente (`opsxHint`), el tutorial
|
|
67
|
+
de Windows lo dice explícito con su propia entrada en *Cuando algo falla*, y la guía del
|
|
68
|
+
dev aclara que la forma con dos puntos es la de Claude.
|
|
69
|
+
- **`dai check` terminaba con código de error en Windows aunque el chequeo pasara**
|
|
70
|
+
(`Assertion failed: !(handle->flags & UV_HANDLE_CLOSING), file src\win\async.c, line 94`).
|
|
71
|
+
Salía con `process.exit()` inmediatamente después del `fetch` al tracker, y en Windows eso
|
|
72
|
+
aborta el proceso mientras undici todavía está desarmando sus handles. Imprimía
|
|
73
|
+
`✅ al día` y devolvía distinto de cero igual: un gate verde reportado como rojo en CI o en
|
|
74
|
+
un hook de git — justo el modo de falla que apaga un gate.
|
|
75
|
+
Ahora el código de salida se fija con `process.exitCode` y el event loop drena solo. Cuesta
|
|
76
|
+
~40 ms (los sockets keep-alive de undici están *unref'd*) y no cambia nada en macOS/Linux.
|
|
77
|
+
El mismo tratamiento va para los `.catch()` del dispatcher de **todos** los comandos que
|
|
78
|
+
salen a la red o spawnean npm (`link-us`, `stamp`, `update-us`, `edit-us`, `forge`,
|
|
79
|
+
`publish`, `pr`/`mr`, `install`, `init`), con `failSoft()`: ahí el comando ya terminó, así
|
|
80
|
+
que cortar de una no aportaba nada y podía tapar el mensaje de error con un stack de C.
|
|
81
|
+
Adentro de un comando `fail()` sigue saliendo de una — ahí sí hay que no volver.
|
|
82
|
+
|
|
83
|
+
### Agregado
|
|
84
|
+
- **Regla nueva en la constitución: *el diseño se aprueba antes de implementar*.** Al terminar
|
|
85
|
+
la propuesta el agente para y pide aprobación explícita; si no tiene una herramienta para
|
|
86
|
+
preguntar, pregunta en texto plano y espera. Vale para los tres asistentes. Es el borde
|
|
87
|
+
QUÉ↔CÓMO, que sí es dominio de dai: sin esa firma, la implementación no tiene contra qué
|
|
88
|
+
revisarse — y el gate no puede depender de que el asistente de turno tenga la herramienta
|
|
89
|
+
correcta. Los repos ya inicializados la reciben con `dai sync`.
|
|
90
|
+
- **`dai doctor` avisa si OpenSpec está por debajo de 1.10.0.** Hasta esa versión, los prompts
|
|
91
|
+
que OpenSpec generaba para Copilot y Cursor nombraban los comandos en la forma de Claude
|
|
92
|
+
(`/opsx:apply`, que ahí no existe) e invocaban `AskUserQuestion`/`TodoWrite`, que solo tiene
|
|
93
|
+
Claude Code. El agente terminaba nombrando comandos inexistentes y salteándose el gate de
|
|
94
|
+
aprobación porque su única forma de preguntar no existía. Está arreglado upstream
|
|
95
|
+
([#727](https://github.com/Fission-AI/OpenSpec/issues/727),
|
|
96
|
+
[#1307](https://github.com/Fission-AI/OpenSpec/issues/1307),
|
|
97
|
+
[#1103](https://github.com/Fission-AI/OpenSpec/issues/1103)), pero un equipo que instaló
|
|
98
|
+
antes se queda con la versión vieja y el síntoma no se parece a la causa.
|
|
99
|
+
- **`dai doctor` reporta los comandos de OpenSpec**, con la forma que le toca a **este** repo:
|
|
100
|
+
`✓ Copilot: /opsx-explore · /opsx-propose · /opsx-apply · /opsx-archive`. Si no están
|
|
101
|
+
generados, dice el comando exacto para generarlos —con el tool id que espera OpenSpec, que
|
|
102
|
+
para Copilot es `github-copilot` y no `copilot`—. Solo mira los asistentes configurados en
|
|
103
|
+
el repo: tener las skills instaladas globalmente no dice nada de los comandos, que son
|
|
104
|
+
archivos versionados.
|
|
105
|
+
|
|
106
|
+
### Cambiado
|
|
107
|
+
- **`introduces` se cierra al TERMINAR de implementar, y lo normal es que lo escriba el
|
|
108
|
+
agente.** El tutorial del dev pedía completarlo justo después de `dai link-us` — cuando
|
|
109
|
+
todavía no se puede saber qué capacidades técnicas va a introducir el change; al empezar
|
|
110
|
+
sería adivinar. Y la skill `link-us` decía explícito "dejar `introduces` para que el dev lo
|
|
111
|
+
liste", cargándole a mano un dato que sabe mejor quien acaba de implementar.
|
|
112
|
+
La frase "el único archivo que se autora **a mano**" empujaba el malentendido: lo que dice
|
|
113
|
+
el ADR-0004 es que es el único registro **autorado** —se escribe, no se deriva—, y `link-us`
|
|
114
|
+
ya resuelve `id`, `version`, `ac_hash`, `change`, `repo` y `autor` por construcción.
|
|
115
|
+
Ahora lo dicen igual el archivo generado, la skill, su template, el tutorial (con el paso de
|
|
116
|
+
cierre después de `/opsx-apply`), la guía del dev, el glosario y el ejemplo end-to-end. El
|
|
117
|
+
**DoD suma el ítem**: `introduces` cerrado, sin el placeholder `<capacidad-tecnica>`.
|
|
118
|
+
- **El DoD nombra los comandos de OpenSpec sin atarlos a un asistente.** Decía
|
|
119
|
+
`opsx:apply` → `opsx:archive`, la forma de Claude, en un template que viaja a los tres.
|
|
120
|
+
- **`DAI_JIRA_FIELDS_FILE` sale comentada en el `.env.dai` que genera `dai init`.** Apuntaba
|
|
121
|
+
al mismo valor que ya usa el CLI por defecto, así que no aportaba nada — pero hacía creer
|
|
122
|
+
que faltaba un archivo obligatorio, y un dev terminó pidiendo los `customfield_*` de la
|
|
123
|
+
empresa para un archivo que **solo hace falta para crear** US, no para leerlas.
|
|
124
|
+
|
|
125
|
+
### Interno
|
|
126
|
+
- **325 tests** (+6 desde 0.12.0): `opsxCommand` y `opsxHint` —que solo Claude lleva los dos
|
|
127
|
+
puntos, y la línea combinada de `dai init` cuando el repo configura varios asistentes—, el
|
|
128
|
+
archivo y el tool id de OpenSpec por asistente (`github-copilot`, que no es `copilot`),
|
|
129
|
+
`OPENSPEC_MIN`, que `DAI_JIRA_FIELDS_FILE` salga comentada, y que la regla nueva de la
|
|
130
|
+
constitución llegue a los tres asistentes.
|
|
131
|
+
|
|
6
132
|
## [0.12.0] — 2026-08-13
|
|
7
133
|
|
|
8
134
|
**La PR deja de robarle la US a otro. `dai pr` resolvía el link recorriendo todo el repo y
|
|
@@ -611,6 +737,8 @@ ClickUp y Jira Cloud.
|
|
|
611
737
|
- Tests de las rutas de red (jira/clickup/forge) con `fetch` mockeado. Sin links rotos;
|
|
612
738
|
`files` de npm sin tests ni secretos.
|
|
613
739
|
|
|
740
|
+
[0.13.1]: https://github.com/dforce2055/dai/releases/tag/v0.13.1
|
|
741
|
+
[0.13.0]: https://github.com/dforce2055/dai/releases/tag/v0.13.0
|
|
614
742
|
[0.12.0]: https://github.com/dforce2055/dai/releases/tag/v0.12.0
|
|
615
743
|
[0.11.0]: https://github.com/dforce2055/dai/releases/tag/v0.11.0
|
|
616
744
|
[0.10.0]: https://github.com/dforce2055/dai/releases/tag/v0.10.0
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.13.1
|
package/cli/dai.mjs
CHANGED
|
@@ -28,11 +28,12 @@ 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
30
|
import { composePrBody, prTitle, forgeTool } from "./lib/pr.mjs";
|
|
31
|
+
import { diagnoseGitSsh, pushFailureHint, WINDOWS_OPENSSH } from "./lib/git-ssh.mjs";
|
|
31
32
|
import { dirsEqual } from "./lib/fsutil.mjs";
|
|
32
33
|
import { parseFlags, parseAssistants, isAssistantToken, asList } from "./lib/args.mjs";
|
|
33
|
-
import { versionDrift, planUpgrade } from "./lib/semver.mjs";
|
|
34
|
+
import { versionDrift, planUpgrade, compareVersions } from "./lib/semver.mjs";
|
|
34
35
|
import { parseSource } from "./lib/skills-source.mjs";
|
|
35
|
-
import { skillToCursor, validateSkill, constitution, constitutionCursorRule, envFor, mergeEnv, upsertBlock, reconcileGitignore, stalePromptFiles } from "./lib/bootstrap.mjs";
|
|
36
|
+
import { skillToCursor, validateSkill, constitution, constitutionCursorRule, envFor, mergeEnv, upsertBlock, reconcileGitignore, stalePromptFiles, opsxHint, opsxCommand, OPSX_COMMAND_FILE, OPENSPEC_TOOL, OPENSPEC_MIN } from "./lib/bootstrap.mjs";
|
|
36
37
|
import { parseFieldsFile, parseFieldOverrides, resolveJiraFields } from "./lib/jira-fields.mjs";
|
|
37
38
|
import { assertProjectKey } from "./lib/pm-jira.mjs";
|
|
38
39
|
import { flattenImplements, stampScope, prScope, matchBranchToImplements, requiresLink, trackerKeysIn } from "./lib/branch-scope.mjs";
|
|
@@ -45,6 +46,13 @@ const HERE = dirname(fileURLToPath(import.meta.url));
|
|
|
45
46
|
process.stdout.on("error", (e) => { if (e.code === "EPIPE") process.exit(0); throw e; });
|
|
46
47
|
|
|
47
48
|
function fail(msg, code = 1) { process.stderr.write("dai: " + msg + "\n"); process.exit(code); }
|
|
49
|
+
|
|
50
|
+
// Igual que fail(), pero sin process.exit(). Lo usan los `.catch()` del dispatcher: ahí el
|
|
51
|
+
// comando YA terminó, así que cortar de una no aporta nada y en Windows cuesta caro — salir
|
|
52
|
+
// de golpe después de un fetch (o de un spawn de npm) aborta el proceso con un assert de
|
|
53
|
+
// libuv y tapa el mensaje de error con un stack de C. Ver la nota en cmdCheckCi.
|
|
54
|
+
// Adentro de un comando `fail()` sigue siendo lo correcto: ahí sí hay que no volver.
|
|
55
|
+
function failSoft(msg, code = 1) { process.stderr.write("dai: " + msg + "\n"); process.exitCode = code; }
|
|
48
56
|
const ok = (m) => process.stdout.write(`✓ ${m}\n`);
|
|
49
57
|
const info = (m) => process.stdout.write(`› ${m}\n`);
|
|
50
58
|
const warn = (m) => process.stdout.write(`⚠ ${m}\n`);
|
|
@@ -219,6 +227,13 @@ function gitCommit() { try { return git(["rev-parse", "HEAD"]); } catch { return
|
|
|
219
227
|
// Quién decide es requiresLink(), leyendo el nombre de la branch.
|
|
220
228
|
//
|
|
221
229
|
// Salidas: 0 = pasa · 1 = falta el link · 2 = hay link pero el QUÉ cambió (atrasado)
|
|
230
|
+
//
|
|
231
|
+
// El código de salida se fija con `process.exitCode`, NO con `process.exit()`. En Windows,
|
|
232
|
+
// salir de golpe justo después de un fetch aborta el proceso con un assert de libuv
|
|
233
|
+
// (`!(handle->flags & UV_HANDLE_CLOSING)`, src\win\async.c) mientras undici todavía está
|
|
234
|
+
// desarmando sus handles. El gate imprimía "al día" y devolvía distinto de cero igual: un
|
|
235
|
+
// verde reportado como rojo en CI o en un hook. Dejar que el loop drene solo cuesta ~40ms
|
|
236
|
+
// (los sockets keep-alive de undici están unref'd) y no cambia nada en macOS/Linux.
|
|
222
237
|
async function cmdCheckCi(opts = {}) {
|
|
223
238
|
const branch = opts.branch || process.env.DAI_CI_BRANCH || ciBranch() || gitBranch();
|
|
224
239
|
const { required, reason } = requiresLink(branch);
|
|
@@ -227,7 +242,7 @@ async function cmdCheckCi(opts = {}) {
|
|
|
227
242
|
info(`branch '${branch || "(desconocida)"}' — ${reason}`);
|
|
228
243
|
if (!required) {
|
|
229
244
|
if (rows.length) info(`igual declara ${rows.length} US (${rows.map((r) => r.id).join(", ")}) — se chequea su cobertura.`);
|
|
230
|
-
else { ok("gate OK — esta branch no requiere US.");
|
|
245
|
+
else { ok("gate OK — esta branch no requiere US."); return; }
|
|
231
246
|
}
|
|
232
247
|
if (required && rows.length === 0) {
|
|
233
248
|
const ids = trackerKeysIn(branch);
|
|
@@ -236,7 +251,7 @@ async function cmdCheckCi(opts = {}) {
|
|
|
236
251
|
` Crealo: dai link-us ${ids[0] || "<ID-DE-LA-US>"}\n` +
|
|
237
252
|
" Si NO implementa una US (tooling, deps, docs), renombrá la branch con un\n" +
|
|
238
253
|
" prefijo exento — chore/, docs/, ci/ — según governance/branch-naming.md.\n");
|
|
239
|
-
process.
|
|
254
|
+
process.exitCode = 1; return;
|
|
240
255
|
}
|
|
241
256
|
|
|
242
257
|
// Hay link: que además esté al día contra la US viva. Sin token/backend eso no se
|
|
@@ -244,7 +259,7 @@ async function cmdCheckCi(opts = {}) {
|
|
|
244
259
|
// con --no-network (o sin adaptador utilizable) valida el link y no más.
|
|
245
260
|
if (opts.noNetwork) {
|
|
246
261
|
ok(`gate OK — ${rows.length} US linkeada(s): ${rows.map((r) => r.id).join(", ")} (--no-network: no se comparó contra la US viva).`);
|
|
247
|
-
process.
|
|
262
|
+
process.exitCode = 0; return;
|
|
248
263
|
}
|
|
249
264
|
loadDaiEnv();
|
|
250
265
|
let adapter;
|
|
@@ -252,7 +267,7 @@ async function cmdCheckCi(opts = {}) {
|
|
|
252
267
|
catch (e) {
|
|
253
268
|
warn(`no puedo comparar contra la US viva: ${e.message}`);
|
|
254
269
|
ok(`gate OK igual — el link existe (${rows.map((r) => r.id).join(", ")}). Configurá el backend para chequear también el atraso.`);
|
|
255
|
-
process.
|
|
270
|
+
process.exitCode = 0; return;
|
|
256
271
|
}
|
|
257
272
|
let worst = 0;
|
|
258
273
|
for (const r of rows) {
|
|
@@ -271,7 +286,7 @@ async function cmdCheckCi(opts = {}) {
|
|
|
271
286
|
}
|
|
272
287
|
}
|
|
273
288
|
if (worst === 0) ok(`gate OK — ${rows.length} US linkeada(s) y al día.`);
|
|
274
|
-
process.
|
|
289
|
+
process.exitCode = worst;
|
|
275
290
|
}
|
|
276
291
|
|
|
277
292
|
// La branch real en CI: en una PR, HEAD es un merge commit detached, así que
|
|
@@ -285,6 +300,7 @@ function ciBranch() {
|
|
|
285
300
|
}
|
|
286
301
|
|
|
287
302
|
// ── check ──────────────────────────────────────────────────────────────────
|
|
303
|
+
// `process.exitCode` en vez de `process.exit()`: ver la nota en cmdCheckCi.
|
|
288
304
|
async function cmdCheck() {
|
|
289
305
|
loadDaiEnv();
|
|
290
306
|
const adapter = getAdapter(process.env);
|
|
@@ -312,7 +328,7 @@ async function cmdCheck() {
|
|
|
312
328
|
for (const id of atrasadas) process.stdout.write(` dai link-us ${id} --resync # re-estampa el ac_hash contra la US viva\n`);
|
|
313
329
|
process.stdout.write(" Después, revisa si tu implementación cubre el criterio nuevo.\n");
|
|
314
330
|
}
|
|
315
|
-
process.
|
|
331
|
+
process.exitCode = worst;
|
|
316
332
|
}
|
|
317
333
|
|
|
318
334
|
// ── stamp ──────────────────────────────────────────────────────────────────
|
|
@@ -975,11 +991,21 @@ async function cmdPr(opts) {
|
|
|
975
991
|
// stdin heredado + GIT_TERMINAL_PROMPT=1: la PRIMERA vez contra un remoto HTTPS
|
|
976
992
|
// corporativo, git/credential-manager necesita poder pedir la credencial. Con stdin
|
|
977
993
|
// ignorado (el default de git()) el login no completaba y el push fallaba en seco.
|
|
978
|
-
|
|
994
|
+
//
|
|
995
|
+
// stderr heredado, y esto no es cosmético: git y ssh PREGUNTAN por stderr. Con
|
|
996
|
+
// stderr en 'pipe' el prompt de una clave con passphrase ("Enter passphrase for
|
|
997
|
+
// key …") caía en el pipe, invisible: el dev veía un cuelgue sin explicación, ssh
|
|
998
|
+
// se rendía y terminaba cayendo a autenticación por password, y lo único que se
|
|
999
|
+
// llegaba a leer era un "Permission denied (publickey…)" que acusa a la clave
|
|
1000
|
+
// cuando la clave estaba bien. Que pregunte a la vista.
|
|
1001
|
+
git(["push", "-u", "origin", branch], { stdio: ["inherit", "pipe", "inherit"], env: { ...process.env, GIT_TERMINAL_PROMPT: "1" } });
|
|
979
1002
|
} catch (e) {
|
|
980
|
-
|
|
1003
|
+
// git ya imprimió su error en vivo (stderr heredado); acá solo va la pista, y esa
|
|
1004
|
+
// pista depende del transporte: "pushea a mano una vez" arregla HTTPS y no arregla
|
|
1005
|
+
// nada en SSH, donde el push a mano falla igual (lib/git-ssh.mjs).
|
|
1006
|
+
const err = String(e.stderr || "").trim();
|
|
981
1007
|
if (err) process.stderr.write(" " + err.split("\n").join("\n ") + "\n");
|
|
982
|
-
process.stdout.write(`
|
|
1008
|
+
for (const l of pushFailureHint(remote, branch)) process.stdout.write(` ${l}\n`);
|
|
983
1009
|
fail(`no pude pushear la branch '${branch}'.`, 1);
|
|
984
1010
|
}
|
|
985
1011
|
|
|
@@ -1263,7 +1289,7 @@ async function cmdInit(repo, opts) {
|
|
|
1263
1289
|
let installOpenspec = opts.openspec === true;
|
|
1264
1290
|
if (!hasOpenspec && opts.openspec === undefined && rl) {
|
|
1265
1291
|
process.stdout.write("\n OpenSpec es el motor recomendado del CÓMO: convierte la US en design + tasks\n");
|
|
1266
|
-
process.stdout.write(
|
|
1292
|
+
process.stdout.write(` (${opsxHint(want)}). No está en este repo — la trazabilidad de dai anda igual sin\n`);
|
|
1267
1293
|
process.stdout.write(" él, pero para el flujo completo conviene tenerlo.\n");
|
|
1268
1294
|
installOpenspec = await askYesNo(rl, "¿Instalar el CLI de OpenSpec ahora? (después ejecutas `openspec init` tú)", false);
|
|
1269
1295
|
}
|
|
@@ -1366,7 +1392,7 @@ async function cmdInit(repo, opts) {
|
|
|
1366
1392
|
.filter(Boolean).join(",") || "claude,github-copilot,cursor";
|
|
1367
1393
|
const osHint = "para sumarlo después: npm i -g @fission-ai/openspec@latest && openspec init --tools " + osTools;
|
|
1368
1394
|
if (hasOpenspec) {
|
|
1369
|
-
ok(
|
|
1395
|
+
ok(`OpenSpec: ya inicializado en el repo — ${opsxHint(want)}`);
|
|
1370
1396
|
} else if (openspecPartial) {
|
|
1371
1397
|
warn("OpenSpec: hay una carpeta openspec/ a medias. Reinicializa: rm -rf openspec && openspec init --tools " + osTools + " --force");
|
|
1372
1398
|
} else if (installOpenspec) {
|
|
@@ -1379,7 +1405,7 @@ async function cmdInit(repo, opts) {
|
|
|
1379
1405
|
try {
|
|
1380
1406
|
info(`OpenSpec: inicializando en el repo (--tools ${osTools})…`);
|
|
1381
1407
|
runNpmTool("openspec", ["init", "--tools", osTools, "--force"], { stdio: "inherit", cwd: repo === "." ? process.cwd() : repo });
|
|
1382
|
-
ok(
|
|
1408
|
+
ok(`OpenSpec: instalado e inicializado — genera design/tasks con ${opsxHint(want)}`);
|
|
1383
1409
|
} catch {
|
|
1384
1410
|
warn("OpenSpec: el CLI está pero falló `openspec init`. Ejecuta a mano en el repo:");
|
|
1385
1411
|
process.stdout.write(` openspec init --tools ${osTools} --force\n`);
|
|
@@ -1645,6 +1671,43 @@ function cmdDoctor() {
|
|
|
1645
1671
|
else warn("falta constitución Cursor (dai-constitution.mdc)");
|
|
1646
1672
|
}
|
|
1647
1673
|
|
|
1674
|
+
// OpenSpec: los comandos que arman el CÓMO. Se reportan acá porque su nombre CAMBIA
|
|
1675
|
+
// según el asistente, y con la forma equivocada el agente no falla — no encuentra nada,
|
|
1676
|
+
// no avisa, y se pone a improvisar salteándose el gate. Verlo escrito ahorra la tarde.
|
|
1677
|
+
const OPSX_IDS = ["explore", "propose", "apply", "archive"];
|
|
1678
|
+
const opsxPath = (kind, id) => join(cwd, ...OPSX_COMMAND_FILE[kind].replace("<id>", id).split("/"));
|
|
1679
|
+
// Solo los asistentes configurados EN ESTE REPO: los comandos opsx son archivos del
|
|
1680
|
+
// repo, así que tener las skills globales no dice nada sobre ellos. Avisar por los otros
|
|
1681
|
+
// dos le pone dos warnings falsos a quien configuró uno solo, que es el caso normal.
|
|
1682
|
+
const localActive = ASSISTANTS.filter((a) => skillNames.some((n) => existsSync(join(a.local, n))));
|
|
1683
|
+
if (localActive.length && existsSync(join(cwd, "openspec"))) {
|
|
1684
|
+
info("OpenSpec — los comandos van en el chat del asistente, no en la terminal:");
|
|
1685
|
+
for (const a of localActive) {
|
|
1686
|
+
const faltan = OPSX_IDS.filter((id) => !existsSync(opsxPath(a.kind, id)));
|
|
1687
|
+
if (faltan.length === OPSX_IDS.length) {
|
|
1688
|
+
warn(`${a.label}: no hay comandos opsx. Generalos: openspec init --tools ${OPENSPEC_TOOL[a.kind]} --force`);
|
|
1689
|
+
} else {
|
|
1690
|
+
const hay = OPSX_IDS.filter((id) => existsSync(opsxPath(a.kind, id)));
|
|
1691
|
+
ok(`${a.label}: ${hay.map((id) => opsxCommand(a.kind, id)).join(" · ")}`);
|
|
1692
|
+
if (faltan.length) warn(`${a.label}: faltan ${faltan.join(", ")} — regenerá con \`openspec init --tools ${OPENSPEC_TOOL[a.kind]} --force\``);
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
process.stdout.write(" (el nombre sale del archivo que genera OpenSpec: solo Claude usa los dos puntos)\n");
|
|
1696
|
+
// La versión importa para algo más que features: hasta OPENSPEC_MIN, los prompts que
|
|
1697
|
+
// OpenSpec genera para Copilot/Cursor decían `/opsx:apply` (la forma de Claude, que ahí
|
|
1698
|
+
// no existe) e invocaban herramientas que solo tiene Claude Code. El agente terminaba
|
|
1699
|
+
// nombrando comandos inexistentes y salteándose el gate de aprobación sin avisar.
|
|
1700
|
+
let osV = null;
|
|
1701
|
+
try { osV = String(runNpmTool("openspec", ["--version"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }) ?? "").trim(); } catch { /* sin CLI */ }
|
|
1702
|
+
if (!osV) info("el CLI de OpenSpec no está en el PATH (los comandos igual viven en el repo)");
|
|
1703
|
+
else if (compareVersions(osV, OPENSPEC_MIN) === -1) {
|
|
1704
|
+
warn(`OpenSpec ${osV} — actualiza a ${OPENSPEC_MIN} o mayor: npm i -g @fission-ai/openspec@latest`);
|
|
1705
|
+
process.stdout.write(" Hasta esa versión, los prompts de Copilot y Cursor nombraban los comandos en la\n");
|
|
1706
|
+
process.stdout.write(" forma de Claude y pedían herramientas que esos asistentes no tienen: el agente se\n");
|
|
1707
|
+
process.stdout.write(" saltea el gate de aprobación y se pone a implementar sin avisar.\n");
|
|
1708
|
+
} else ok(`OpenSpec ${osV}`);
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1648
1711
|
info("adaptador de PM:");
|
|
1649
1712
|
const pm = process.env.DAI_PM || "md";
|
|
1650
1713
|
ok(`DAI_PM=${pm}`);
|
|
@@ -1676,6 +1739,44 @@ function cmdDoctor() {
|
|
|
1676
1739
|
: warn("DAI_CLICKUP_LIST_ID vacío — solo hace falta para `dai publish` (crear tareas)");
|
|
1677
1740
|
}
|
|
1678
1741
|
|
|
1742
|
+
// ── forge: el CÓMO sale del repo por acá ─────────────────────────────────────
|
|
1743
|
+
// Si el push no sale, no hay PR; sin PR no hay link QUÉ↔CÓMO. El chequeo del cliente
|
|
1744
|
+
// ssh existe porque su modo de fallar miente: ver lib/git-ssh.mjs.
|
|
1745
|
+
const remote = gitRemote();
|
|
1746
|
+
info("forge:");
|
|
1747
|
+
if (!remote) warn("no hay remoto 'origin' — `dai pr` no tiene dónde publicar la branch");
|
|
1748
|
+
else {
|
|
1749
|
+
const p = parseRemote(remote);
|
|
1750
|
+
ok(`remoto origin: ${remote}${p ? ` (${detectForge(p.host)})` : ""}`);
|
|
1751
|
+
let sshConfig = null;
|
|
1752
|
+
try { sshConfig = git(["config", "--get", "core.sshCommand"]) || null; } catch { /* sin valor: git sale 1 */ }
|
|
1753
|
+
const d = diagnoseGitSsh({
|
|
1754
|
+
platform: process.platform,
|
|
1755
|
+
remote,
|
|
1756
|
+
config: sshConfig,
|
|
1757
|
+
env: { GIT_SSH_COMMAND: process.env.GIT_SSH_COMMAND, GIT_SSH: process.env.GIT_SSH },
|
|
1758
|
+
hasWindowsOpenSsh: existsSync(WINDOWS_OPENSSH),
|
|
1759
|
+
});
|
|
1760
|
+
if (d.status === "ok") ok(`git usa el OpenSSH de Windows (ve al ssh-agent)`);
|
|
1761
|
+
if (d.status === "ok-por-env") {
|
|
1762
|
+
ok(`git usa el OpenSSH de Windows (ve al ssh-agent)`);
|
|
1763
|
+
warn(`pero viene de ${d.source}, no del config: se pierde al cerrar la terminal.`);
|
|
1764
|
+
process.stdout.write(` Para que quede: git config --global core.sshCommand "${WINDOWS_OPENSSH}"\n`);
|
|
1765
|
+
}
|
|
1766
|
+
if (d.status === "bundled" || d.status === "otro-ssh") {
|
|
1767
|
+
warn(d.status === "bundled"
|
|
1768
|
+
? "git usa su propio ssh (Git for Windows), que NO ve al ssh-agent de Windows."
|
|
1769
|
+
: `git usa ${d.bin} (por ${d.source}), que probablemente no vea al ssh-agent de Windows.`);
|
|
1770
|
+
process.stdout.write(` Si tu clave tiene passphrase, cada push te la va a pedir — y si el prompt no llega,\n`);
|
|
1771
|
+
process.stdout.write(` ssh cae a password y falla con "Permission denied (publickey…)", que acusa a la clave.\n`);
|
|
1772
|
+
if (d.fix) process.stdout.write(` → git config --global core.sshCommand "${d.fix}"\n`);
|
|
1773
|
+
if (d.shadowed) process.stdout.write(` Ojo: ${d.source} está seteada y pisa tu core.sshCommand. Límpiala primero.\n`);
|
|
1774
|
+
}
|
|
1775
|
+
if (d.status === "bundled-sin-openssh") {
|
|
1776
|
+
warn("git usa su propio ssh y no encontré el OpenSSH de Windows: si el push pide passphrase, instálalo (Configuración → Características opcionales → Cliente OpenSSH).");
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1679
1780
|
// ── version-drift del scaffold vs el CLI (ADR-0010) ──────────────────────────
|
|
1680
1781
|
if (existsSync(join(process.cwd(), ".dai", "VERSION"))) { info("versión del scaffold:"); reportDrift(); }
|
|
1681
1782
|
}
|
|
@@ -1693,23 +1794,23 @@ const { opts, pos } = parseFlags(rest);
|
|
|
1693
1794
|
switch (cmd) {
|
|
1694
1795
|
case "ac-hash": cmdAcHash(pos[0]); break;
|
|
1695
1796
|
case "ls": cmdLs(opts); break;
|
|
1696
|
-
case "link-us": cmdLinkUs(pos[0], opts).catch((e) =>
|
|
1697
|
-
case "check": (opts.ci ? cmdCheckCi(opts) : cmdCheck()).catch((e) =>
|
|
1698
|
-
case "stamp": cmdStamp(pos, opts).catch((e) =>
|
|
1699
|
-
case "update-us": cmdUpdateUs(pos[0], opts).catch((e) =>
|
|
1700
|
-
case "edit-us": cmdEditUs(pos[0], opts).catch((e) =>
|
|
1701
|
-
case "forge": cmdForge(pos[0], pos[1], opts).catch((e) =>
|
|
1702
|
-
case "publish": cmdPublish(pos[0], opts).catch((e) =>
|
|
1797
|
+
case "link-us": cmdLinkUs(pos[0], opts).catch((e) => failSoft(String(e.message))); break;
|
|
1798
|
+
case "check": (opts.ci ? cmdCheckCi(opts) : cmdCheck()).catch((e) => failSoft(String(e.message))); break;
|
|
1799
|
+
case "stamp": cmdStamp(pos, opts).catch((e) => failSoft(String(e.message))); break;
|
|
1800
|
+
case "update-us": cmdUpdateUs(pos[0], opts).catch((e) => failSoft(String(e.message))); break;
|
|
1801
|
+
case "edit-us": cmdEditUs(pos[0], opts).catch((e) => failSoft(String(e.message))); break;
|
|
1802
|
+
case "forge": cmdForge(pos[0], pos[1], opts).catch((e) => failSoft(String(e.message))); break;
|
|
1803
|
+
case "publish": cmdPublish(pos[0], opts).catch((e) => failSoft(String(e.message))); break;
|
|
1703
1804
|
case "pr":
|
|
1704
|
-
case "mr": cmdPr(opts).catch((e) =>
|
|
1805
|
+
case "mr": cmdPr(opts).catch((e) => failSoft(String(e.message))); break; // `mr` = alias para GitLab (merge request)
|
|
1705
1806
|
case "done": cmdDone(opts); break;
|
|
1706
1807
|
case "archive": cmdArchive(pos[0], opts); break;
|
|
1707
|
-
case "install": cmdInstall(opts).catch((e) =>
|
|
1808
|
+
case "install": cmdInstall(opts).catch((e) => failSoft(String(e.message))); break; // alias de `dai skills install`
|
|
1708
1809
|
case "skills":
|
|
1709
|
-
if (pos[0] === "install" || pos[0] === undefined) cmdInstall(opts).catch((e) =>
|
|
1810
|
+
if (pos[0] === "install" || pos[0] === undefined) cmdInstall(opts).catch((e) => failSoft(String(e.message)));
|
|
1710
1811
|
else fail(`subcomando de skills desconocido: '${pos[0]}' (por ahora: install)`, 2);
|
|
1711
1812
|
break;
|
|
1712
|
-
case "init": cmdInit(pos[0], opts).catch((e) =>
|
|
1813
|
+
case "init": cmdInit(pos[0], opts).catch((e) => failSoft(String(e.message))); break;
|
|
1713
1814
|
case "sync": cmdSync(pos[0], opts); break;
|
|
1714
1815
|
case "upgrade":
|
|
1715
1816
|
case "update": cmdUpgrade(opts); break;
|
package/cli/lib/bootstrap.mjs
CHANGED
|
@@ -106,6 +106,44 @@ export function validateSkill(md) {
|
|
|
106
106
|
// cada `/comando` con una copia vieja y sin templates.
|
|
107
107
|
export const stalePromptFiles = (skills) => skills.map((n) => `${n}.prompt.md`);
|
|
108
108
|
|
|
109
|
+
// OpenSpec nombra sus comandos DISTINTO según el asistente, y no es cosmético:
|
|
110
|
+
// Claude .claude/commands/opsx/<id>.md → /opsx:propose (namespace anidado)
|
|
111
|
+
// Copilot .github/prompts/opsx-<id>.prompt.md → /opsx-propose (nombre aplanado)
|
|
112
|
+
// Cursor .cursor/commands/opsx-<id>.md → /opsx-propose
|
|
113
|
+
// Tipear `/opsx:propose` en Copilot no matchea NADA: el agente no carga el workflow y se
|
|
114
|
+
// pone a improvisar — se saltea el gate propose → aprobación → apply sin avisarle a nadie.
|
|
115
|
+
// Decirle la forma correcta al dev es la diferencia entre el método y vibe coding.
|
|
116
|
+
export function opsxCommand(kind, name = "propose") {
|
|
117
|
+
return kind === "claude" ? `/opsx:${name}` : `/opsx-${name}`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// El archivo del que sale ese nombre — lo que hay que mirar cuando el comando "no existe".
|
|
121
|
+
export const OPSX_COMMAND_FILE = {
|
|
122
|
+
claude: ".claude/commands/opsx/<id>.md",
|
|
123
|
+
copilot: ".github/prompts/opsx-<id>.prompt.md",
|
|
124
|
+
cursor: ".cursor/commands/opsx-<id>.md",
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
// Desde esta versión, OpenSpec escribe el nombre del comando que ENTIENDE cada asistente
|
|
128
|
+
// dentro del cuerpo del prompt (antes todos decían `/opsx:apply`, la forma de Claude) y no
|
|
129
|
+
// invoca herramientas que solo existen en Claude Code (`AskUserQuestion`, `TodoWrite`).
|
|
130
|
+
// Con una anterior, un dev de Copilot ve un agente que le nombra comandos inexistentes y
|
|
131
|
+
// que se pasa de largo el gate de aprobación porque su única forma de preguntar no existe.
|
|
132
|
+
export const OPENSPEC_MIN = "1.10.0";
|
|
133
|
+
|
|
134
|
+
// El id con el que OpenSpec conoce a cada asistente en `openspec init --tools`.
|
|
135
|
+
export const OPENSPEC_TOOL = { claude: "claude", copilot: "github-copilot", cursor: "cursor" };
|
|
136
|
+
|
|
137
|
+
// La misma info, resumida para una sola línea de `dai init` (que puede haber configurado
|
|
138
|
+
// varios asistentes a la vez).
|
|
139
|
+
export function opsxHint(want, name = "propose") {
|
|
140
|
+
const dash = [want?.copilot && "Copilot", want?.cursor && "Cursor"].filter(Boolean).join(" y ");
|
|
141
|
+
const forms = [];
|
|
142
|
+
if (want?.claude) forms.push(`${opsxCommand("claude", name)} (Claude)`);
|
|
143
|
+
if (dash) forms.push(`${opsxCommand("copilot", name)} (${dash})`);
|
|
144
|
+
return forms.join(" · ") || opsxCommand("claude", name);
|
|
145
|
+
}
|
|
146
|
+
|
|
109
147
|
// Transforma un SKILL.md (Claude) en un SKILL.md de Cursor.
|
|
110
148
|
// Conserva name/description/body y ajusta solo el frontmatter.
|
|
111
149
|
export function skillToCursor(md) {
|
|
@@ -140,8 +178,10 @@ export function envFor(pm) {
|
|
|
140
178
|
"# La clave del PROYECTO (p. ej. PROJ), no la de un ticket (PROJ-123).\n" +
|
|
141
179
|
"DAI_JIRA_PROJECT=\n" +
|
|
142
180
|
"DAI_JIRA_ISSUETYPE=Story\n" +
|
|
143
|
-
"#
|
|
144
|
-
"
|
|
181
|
+
"# Solo si tu Jira exige campos propios AL CREAR una US (lo usa grill-user-story,\n" +
|
|
182
|
+
"# no hace falta para leerlas). El default ya es .dai/jira-fields.json; descomentá\n" +
|
|
183
|
+
"# solo para apuntar a otra ruta. Si el archivo no existe, se ignora.\n" +
|
|
184
|
+
"# DAI_JIRA_FIELDS_FILE=.dai/jira-fields.json\n";
|
|
145
185
|
}
|
|
146
186
|
return head + "DAI_PM=md\nDAI_MD_US_DIR=.dai/us\n";
|
|
147
187
|
}
|
|
@@ -230,6 +270,7 @@ export function constitution(kind) {
|
|
|
230
270
|
## Reglas
|
|
231
271
|
|
|
232
272
|
- **No vibe coding:** toda implementación arranca de una US con criterios testeables.
|
|
273
|
+
- **El diseño se aprueba antes de implementar:** cuando termines la propuesta (proposal / design / tasks), **para y pide aprobación explícita**. No sigas de largo porque "ya está claro": el CÓMO lo firma la persona, y una implementación que empieza antes de esa firma no tiene contra qué revisarse. Si no tienes una herramienta para preguntar, pregunta en texto plano y espera la respuesta.
|
|
233
274
|
- **TDD:** test primero, por la interfaz pública; sobrevive a un refactor.
|
|
234
275
|
- **El link se autora una vez** (\`implements.yaml\`); la cobertura se **deriva** (nunca a mano).
|
|
235
276
|
- **Verifica el comportamiento, no solo que compile:** que pase el chequeo estático o el build no prueba que funcione; ejercita el flujo real antes de darlo por hecho.
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// dai · qué cliente SSH usa git, y si ese cliente llega al agente (Windows).
|
|
2
|
+
//
|
|
3
|
+
// En Windows conviven dos ssh.exe: el de OpenSSH for Windows (C:\Windows\System32\
|
|
4
|
+
// OpenSSH), que habla con el servicio `ssh-agent`, y el que trae Git for Windows
|
|
5
|
+
// (MSYS), que NO lo ve. Con una clave con passphrase el síntoma es desconcertante:
|
|
6
|
+
// `ssh -T` autentica (la firma la hace el agente) y `git push` pide la passphrase o,
|
|
7
|
+
// si nadie la contesta, cae a autenticación por password y muere con
|
|
8
|
+
// "Permission denied (publickey…)" — un error que apunta al lugar equivocado y se
|
|
9
|
+
// come una mañana buscando el problema en la clave, en el token o en el server.
|
|
10
|
+
//
|
|
11
|
+
// Núcleo puro: el I/O (git config, existsSync) vive en dai.mjs.
|
|
12
|
+
|
|
13
|
+
export const WINDOWS_OPENSSH = "C:/Windows/System32/OpenSSH/ssh.exe";
|
|
14
|
+
|
|
15
|
+
// ¿Por dónde habla el remoto? Decide qué consejo tiene sentido cuando el push falla.
|
|
16
|
+
export function remoteTransport(remote) {
|
|
17
|
+
const r = String(remote ?? "").trim();
|
|
18
|
+
if (!r) return null;
|
|
19
|
+
if (/^ssh:\/\//i.test(r)) return "ssh";
|
|
20
|
+
if (/^https?:\/\//i.test(r)) return "https";
|
|
21
|
+
if (/^[\w.-]+@[^:]+:/.test(r)) return "ssh"; // scp-like: git@host:org/repo.git
|
|
22
|
+
return null; // git://, file://, ruta local…
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// El binario de un core.sshCommand / GIT_SSH_COMMAND, que puede traer argumentos y
|
|
26
|
+
// comillas: `"C:/Program Files/Git/usr/bin/ssh.exe" -v` → la ruta sola.
|
|
27
|
+
export function sshBinaryOf(cmd) {
|
|
28
|
+
const s = String(cmd ?? "").trim();
|
|
29
|
+
if (!s) return null;
|
|
30
|
+
if (s[0] === '"' || s[0] === "'") {
|
|
31
|
+
const end = s.indexOf(s[0], 1);
|
|
32
|
+
return end === -1 ? s.slice(1) : s.slice(1, end);
|
|
33
|
+
}
|
|
34
|
+
const sp = s.search(/\s/);
|
|
35
|
+
return sp === -1 ? s : s.slice(0, sp);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ¿Esa ruta es el OpenSSH de Windows? Normalizado: mayúsculas y `\` vs `/` varían
|
|
39
|
+
// según quién haya escrito el config (git acepta las dos formas).
|
|
40
|
+
export function isWindowsOpenSsh(bin) {
|
|
41
|
+
const p = String(bin ?? "").replace(/\\/g, "/").toLowerCase();
|
|
42
|
+
return /(^|\/)system32\/openssh\/ssh(\.exe)?$/.test(p);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Diagnóstico del cliente ssh que va a usar git. Entradas ya resueltas por el
|
|
46
|
+
// llamador; acá no se toca el disco ni se lanza un proceso.
|
|
47
|
+
// platform — process.platform
|
|
48
|
+
// remote — url de origin (o null)
|
|
49
|
+
// config — `git config --get core.sshCommand` (o null si no está)
|
|
50
|
+
// env — { GIT_SSH_COMMAND, GIT_SSH }
|
|
51
|
+
// hasWindowsOpenSsh — existsSync(WINDOWS_OPENSSH)
|
|
52
|
+
//
|
|
53
|
+
// status:
|
|
54
|
+
// n/a → no aplica (no es Windows, o el remoto no habla SSH)
|
|
55
|
+
// ok → apunta al OpenSSH de Windows por core.sshCommand
|
|
56
|
+
// ok-por-env → apunta bien, pero desde una env var: se pierde al cerrar la terminal
|
|
57
|
+
// otro-ssh → apunta a otro ssh (el de MSYS, plink…)
|
|
58
|
+
// bundled → nadie lo configuró: git usa el suyo, que no ve el agente
|
|
59
|
+
// bundled-sin-openssh → igual, pero no hay OpenSSH de Windows que recomendar
|
|
60
|
+
export function diagnoseGitSsh({ platform, remote, config, env = {}, hasWindowsOpenSsh = false } = {}) {
|
|
61
|
+
if (platform !== "win32") return { status: "n/a", reason: "no-windows" };
|
|
62
|
+
const transport = remoteTransport(remote);
|
|
63
|
+
if (transport !== "ssh") return { status: "n/a", reason: transport ? "remoto-https" : "sin-remoto" };
|
|
64
|
+
|
|
65
|
+
// Precedencia real de git: GIT_SSH_COMMAND > GIT_SSH > core.sshCommand.
|
|
66
|
+
const [source, raw] =
|
|
67
|
+
env.GIT_SSH_COMMAND ? ["GIT_SSH_COMMAND", env.GIT_SSH_COMMAND]
|
|
68
|
+
: env.GIT_SSH ? ["GIT_SSH", env.GIT_SSH]
|
|
69
|
+
: config ? ["core.sshCommand", config]
|
|
70
|
+
: ["default", null];
|
|
71
|
+
|
|
72
|
+
// Una env var pisando un core.sshCommand que ya estaba bien: el dev "arregló" el
|
|
73
|
+
// config, no funciona, y no hay forma de verlo mirando el .gitconfig.
|
|
74
|
+
const shadowed = source !== "default" && source !== "core.sshCommand" && Boolean(config);
|
|
75
|
+
|
|
76
|
+
if (source === "default") {
|
|
77
|
+
return hasWindowsOpenSsh
|
|
78
|
+
? { status: "bundled", source, bin: null, fix: WINDOWS_OPENSSH, shadowed }
|
|
79
|
+
: { status: "bundled-sin-openssh", source, bin: null, fix: null, shadowed };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const bin = sshBinaryOf(raw);
|
|
83
|
+
if (isWindowsOpenSsh(bin)) {
|
|
84
|
+
return { status: source === "core.sshCommand" ? "ok" : "ok-por-env", source, bin, fix: null, shadowed };
|
|
85
|
+
}
|
|
86
|
+
return { status: "otro-ssh", source, bin, fix: hasWindowsOpenSsh ? WINDOWS_OPENSSH : null, shadowed };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Qué decirle al dev cuando `git push` falla. El consejo de "pusheá a mano una vez"
|
|
90
|
+
// solo aplica a HTTPS, donde el credential manager pide la credencial la primera vez;
|
|
91
|
+
// contra un remoto SSH el push a mano falla EXACTAMENTE igual, así que mandarlo por ahí
|
|
92
|
+
// es hacerle perder el tiempo mientras el problema real (la clave, el agente) sigue ahí.
|
|
93
|
+
export function pushFailureHint(remote, branch) {
|
|
94
|
+
if (remoteTransport(remote) === "ssh") {
|
|
95
|
+
return [
|
|
96
|
+
`El remoto habla SSH: en el push no interviene el token de gh/glab, solo tu clave.`,
|
|
97
|
+
`Prueba a mano — así ves lo que ssh pregunta (p. ej. la passphrase de tu clave):`,
|
|
98
|
+
` git push -u origin ${branch}`,
|
|
99
|
+
`Si te la pide en cada push, tu clave está en un agente que git no ve: dai doctor`,
|
|
100
|
+
];
|
|
101
|
+
}
|
|
102
|
+
return [
|
|
103
|
+
`Si es la primera vez contra este remoto, autentica pusheando a mano una vez:`,
|
|
104
|
+
` git push -u origin ${branch}`,
|
|
105
|
+
`y vuelve a ejecutar: dai pr`,
|
|
106
|
+
];
|
|
107
|
+
}
|
package/cli/lib/link-us.mjs
CHANGED
|
@@ -41,7 +41,9 @@ export function extractTitle(md) {
|
|
|
41
41
|
|
|
42
42
|
// Render del implements.yaml (schema ADR-0004).
|
|
43
43
|
export function renderImplementsYaml({ change, repo, id, version = "v1", ac_hash, autor }) {
|
|
44
|
-
return `# Link QUÉ↔CÓMO · scaffoldeado por dai link-us. El ÚNICO link
|
|
44
|
+
return `# Link QUÉ↔CÓMO · scaffoldeado por dai link-us. El ÚNICO link AUTORADO del método:
|
|
45
|
+
# lo escribe alguien, no se deriva. Todo lo de abajo ya está resuelto salvo 'introduces',
|
|
46
|
+
# que se completa AL TERMINAR de implementar — recién ahí se sabe qué se introdujo.
|
|
45
47
|
# Schema: docs/adr/0004-ubicacion-y-schema-implements.md
|
|
46
48
|
change: ${change}
|
|
47
49
|
repo: ${repo}
|
|
@@ -52,7 +54,8 @@ implements:
|
|
|
52
54
|
ac_hash: ${ac_hash}
|
|
53
55
|
|
|
54
56
|
introduces:
|
|
55
|
-
- <capacidad-tecnica> #
|
|
57
|
+
- <capacidad-tecnica> # al cerrar la implementación: capacidades técnicas nuevas
|
|
58
|
+
# (o borrá el bloque si no hay ninguna)
|
|
56
59
|
|
|
57
60
|
autor: ${autor}
|
|
58
61
|
`;
|
|
@@ -157,7 +157,7 @@ $ dai link-us ABC-482 --us us.md --change finalizar-compra
|
|
|
157
157
|
```
|
|
158
158
|
|
|
159
159
|
```yaml
|
|
160
|
-
# implements.yaml — el ÚNICO link
|
|
160
|
+
# implements.yaml — el ÚNICO link AUTORADO: se escribe, no se deriva (schema ADR-0004)
|
|
161
161
|
change: finalizar-compra
|
|
162
162
|
repo: frontend
|
|
163
163
|
|
package/docs/glosario.md
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
| Término | Qué es |
|
|
21
21
|
|---|---|
|
|
22
22
|
| **Link (QUÉ↔CÓMO)** | La relación entre un requerimiento y su implementación. |
|
|
23
|
-
| **`implements`** | La declaración `implements: <id>@<version>` en el código. El **único** link autorado
|
|
23
|
+
| **`implements`** | La declaración `implements: <id>@<version>` en el código. El **único** link autorado (se escribe, no se deriva): lo scaffoldea `dai link-us` y se cierra al terminar de implementar. |
|
|
24
24
|
| **`implements.yaml`** | El archivo, en el change del repo, que contiene ese link. Lo genera `link-us`. Ejemplo lleno + árbol de dónde vive entre los artefactos de OpenSpec: [ADR-0004](adr/0004-ubicacion-y-schema-implements.md). |
|
|
25
25
|
| **Trazabilidad inversa / cobertura** | El mapa "quién implementó este QUÉ". **Se genera, nunca se escribe.** |
|
|
26
26
|
| **Índice / router** | La tabla central que dice qué ID vive en qué repos. Es un router, **no un almacén**: no guarda el detalle. |
|
package/docs/guias/dev.md
CHANGED
|
@@ -7,7 +7,10 @@
|
|
|
7
7
|
|
|
8
8
|
- El **CÓMO**: diseño técnico, modelo de datos, arquitectura de la solución.
|
|
9
9
|
- Las **tareas técnicas** (las derivas tú, desde la US, con OpenSpec).
|
|
10
|
-
- El **link** (`implements.yaml`): es el **único** que se autora
|
|
10
|
+
- El **link** (`implements.yaml`): es el **único** que se autora, no se deriva ([Art. 9](../MANIFIESTO.md#art-9)).
|
|
11
|
+
`dai link-us` lo scaffoldea con el `id`/`version`/`ac_hash` ya resueltos; lo único que queda
|
|
12
|
+
abierto es `introduces`, y se cierra **al terminar de implementar** —normalmente lo completa
|
|
13
|
+
el agente que implementó, y tú lo revisas en la PR—.
|
|
11
14
|
- El **código** y su **spec técnica**.
|
|
12
15
|
|
|
13
16
|
## Lo que NO tocas
|
|
@@ -88,3 +91,10 @@ que está junto a tu `implements.yaml`.
|
|
|
88
91
|
- `dai check` · `dai pr` · `dai stamp` · `dai done` (limpieza, opcional)
|
|
89
92
|
- `dai update-us` — empuja al tracker una US que refinaste implementando
|
|
90
93
|
- `definition-of-done.md`
|
|
94
|
+
|
|
95
|
+
> **Ojo con el nombre de los comandos `opsx`.** Acá se escriben en la forma de Claude Code
|
|
96
|
+
> (`/opsx:apply`). En **Copilot y Cursor** los mismos comandos van **con guion**:
|
|
97
|
+
> `/opsx-apply`. No es un alias: es el nombre del archivo que genera OpenSpec para cada
|
|
98
|
+
> asistente. Con la forma equivocada el agente no encuentra el comando, no carga el workflow
|
|
99
|
+
> y se pone a improvisar sin avisar. `dai init` te dice cuál te toca.
|
|
100
|
+
|
|
@@ -135,7 +135,7 @@ dai init --for copilot --pm jira
|
|
|
135
135
|
```
|
|
136
136
|
|
|
137
137
|
Te va a preguntar si quieres instalar **OpenSpec**: responde **`s`**. Es el motor del CÓMO —
|
|
138
|
-
convierte la US en `design.md` + `tasks.md` con los comandos `/opsx
|
|
138
|
+
convierte la US en `design.md` + `tasks.md` con los comandos `/opsx-*`. Un dev sí lo usa.
|
|
139
139
|
|
|
140
140
|
Lo que deja:
|
|
141
141
|
|
|
@@ -262,28 +262,40 @@ Trae la US de Jira, calcula el `ac_hash` de sus criterios y deja dos cosas:
|
|
|
262
262
|
✓ archivo: openspec/changes/finalizar-la-compra-del-carrito/implements.yaml (ac_hash 380d814b)
|
|
263
263
|
```
|
|
264
264
|
|
|
265
|
-
Ese `implements.yaml` es
|
|
266
|
-
([ADR-0004](../adr/0004-ubicacion-y-schema-implements.md)):
|
|
265
|
+
Ese `implements.yaml` es el **único registro autorado** del método
|
|
266
|
+
([ADR-0004](../adr/0004-ubicacion-y-schema-implements.md)): el resto de la trazabilidad se
|
|
267
|
+
**deriva**, este se escribe. "Autorado" significa que lo escribe alguien —no que lo tipees tú
|
|
268
|
+
ahora—, y `dai link-us` ya te lo dejó casi entero:
|
|
267
269
|
|
|
268
270
|
```yaml
|
|
269
271
|
change: finalizar-la-compra-del-carrito
|
|
270
272
|
repo: tienda
|
|
271
273
|
|
|
272
274
|
implements:
|
|
273
|
-
- id: PROJ-125
|
|
274
|
-
version: v1
|
|
275
|
-
ac_hash: 380d814b
|
|
275
|
+
- id: PROJ-125 # ← ya está: lo puso link-us desde el ID que validó
|
|
276
|
+
version: v1 # ← ya está
|
|
277
|
+
ac_hash: 380d814b # ← ya está: calculado sobre los criterios de la US
|
|
276
278
|
|
|
277
279
|
introduces:
|
|
278
|
-
- <capacidad-tecnica> #
|
|
280
|
+
- <capacidad-tecnica> # ← lo ÚNICO pendiente, y NO se completa ahora
|
|
279
281
|
|
|
280
|
-
autor: tu.nombre
|
|
282
|
+
autor: tu.nombre # ← ya está
|
|
281
283
|
```
|
|
282
284
|
|
|
283
|
-
|
|
285
|
+
> **No completes `introduces` todavía.** Es lo que este change **introduce** en el repo: las
|
|
286
|
+
> capacidades técnicas nuevas. Recién al terminar de implementar se sabe cuáles son —al
|
|
287
|
+
> empezar sería adivinar—, así que se completa **al cerrar la implementación**, no acá.
|
|
288
|
+
|
|
289
|
+
**Y normalmente no lo escribes tú: lo escribe el agente.** Cuando `/opsx-apply` termina de
|
|
290
|
+
aplicar las tareas, el que implementó es el que sabe qué capacidades quedaron: pídele que
|
|
291
|
+
complete `introduces` (o que borre el bloque si el change no introdujo ninguna) como último
|
|
292
|
+
paso. **Tú lo revisas en la MR** — el archivo va versionado con el código justamente para que
|
|
293
|
+
se lea en la revisión, igual que cualquier otro cambio.
|
|
284
294
|
|
|
285
295
|
> **El key no se tipea nunca a mano.** La rama y el `implements.yaml` salen los dos del mismo
|
|
286
|
-
> ID validado: por eso el link no puede quedar mal escrito.
|
|
296
|
+
> ID validado: por eso el link no puede quedar mal escrito. El campo que sí requiere criterio
|
|
297
|
+
> humano —`introduces`— es el único que queda abierto, y no es un dato del tracker: es tu
|
|
298
|
+
> lectura de lo que el change agregó.
|
|
287
299
|
|
|
288
300
|
> **Si te dice que la US no tiene criterios de aceptación**, frena: no es un problema de tu
|
|
289
301
|
> setup. Esa US no cumple el [DoR](../../templates/definition-of-ready.md) y vuelve al PO.
|
|
@@ -291,14 +303,36 @@ Completa `introduces` con las capacidades técnicas nuevas del change (o bórral
|
|
|
291
303
|
### 2. Arma el CÓMO y programa
|
|
292
304
|
|
|
293
305
|
```
|
|
294
|
-
/opsx
|
|
295
|
-
/opsx
|
|
296
|
-
/opsx
|
|
306
|
+
/opsx-explore → entender el terreno
|
|
307
|
+
/opsx-propose → design.md + tasks.md sobre la rama ya linkeada
|
|
308
|
+
/opsx-apply → el agente implementa las tareas con test primero (/tdd)
|
|
297
309
|
```
|
|
298
310
|
|
|
311
|
+
> **En Copilot los comandos van con guion, no con dos puntos.** OpenSpec nombra sus comandos
|
|
312
|
+
> según el asistente: en Claude Code son `/opsx:propose` (namespace anidado), y en Copilot y
|
|
313
|
+
> Cursor son `/opsx-propose`, por el nombre del archivo que generan
|
|
314
|
+
> (`.github/prompts/opsx-propose.prompt.md`). Mucha documentación —la de OpenSpec incluida—
|
|
315
|
+
> muestra solo la forma de Claude.
|
|
316
|
+
>
|
|
317
|
+
> Si tipeas `/opsx:propose` en Copilot **no falla con un error**: no encuentra nada, se queda
|
|
318
|
+
> con tu texto suelto y se pone a improvisar. Se ve como un agente que "hace lo que quiere",
|
|
319
|
+
> que se saltea el gate y que arranca a programar sin haber escrito la propuesta. Si te pasa
|
|
320
|
+
> eso, lo primero que hay que mirar es cómo escribiste el comando.
|
|
321
|
+
|
|
299
322
|
Tú validas el diseño, decides qué comportamientos importa testear y **revisas lo que escribió
|
|
300
323
|
el agente**: eres responsable del código, no la IA.
|
|
301
324
|
|
|
325
|
+
Cuando `/opsx-apply` termina, cierra el link — es el momento en que ya se sabe qué introdujo
|
|
326
|
+
el change:
|
|
327
|
+
|
|
328
|
+
```
|
|
329
|
+
Completa `introduces` en el implements.yaml con las capacidades técnicas que agregó este
|
|
330
|
+
change (o borra el bloque si no agregó ninguna).
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
Revisa lo que puso. `introduces` no es un dato del tracker: es la lectura de lo que el change
|
|
334
|
+
agregó al repo, y viaja versionado para que se lea en la MR.
|
|
335
|
+
|
|
302
336
|
### 3. Commitea y comprueba el link
|
|
303
337
|
|
|
304
338
|
```powershell
|
|
@@ -382,12 +416,58 @@ chat de Copilot en modo *Agent*, escribiendo `/` adelante.
|
|
|
382
416
|
|---|---|---|
|
|
383
417
|
| Los comandos de **dai** | **PowerShell**, parado en el repositorio | `dai link-us` · `dai check` · `dai mr` · `dai stamp` · `dai done` |
|
|
384
418
|
| Las **skills** (empiezan con `/`) | El **chat de Copilot**, modo *Agent* | `/link-us` · `/tdd` · `/dai-review` · `/grill-intent` |
|
|
385
|
-
| Los comandos de **OpenSpec** | El **chat de Copilot** | `/opsx
|
|
419
|
+
| Los comandos de **OpenSpec** | El **chat de Copilot** | `/opsx-explore` · `/opsx-propose` · `/opsx-apply` (con **guion**: ver arriba) |
|
|
386
420
|
|
|
387
421
|
---
|
|
388
422
|
|
|
389
423
|
## Cuando algo falla
|
|
390
424
|
|
|
425
|
+
### El agente no respeta los pasos: implementa sin escribir la propuesta
|
|
426
|
+
|
|
427
|
+
Síntoma: le pides `/opsx:explore` o `/opsx:propose` y el agente te explica el método, te pide
|
|
428
|
+
que le pegues el ticket a mano, o directamente se pone a escribir código sin haber generado
|
|
429
|
+
`proposal.md` / `design.md` / `tasks.md`. Hay que frenarlo a mano en cada paso.
|
|
430
|
+
|
|
431
|
+
Casi siempre es **el nombre del comando**. En Copilot son `/opsx-explore`, `/opsx-propose`,
|
|
432
|
+
`/opsx-apply` — **con guion**. Con los dos puntos, Copilot no encuentra ningún comando, no
|
|
433
|
+
avisa, y toma tu texto como una charla suelta: el workflow nunca se cargó. Comprueba qué
|
|
434
|
+
tienes disponible:
|
|
435
|
+
|
|
436
|
+
```powershell
|
|
437
|
+
dai doctor
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
En el apartado **OpenSpec** te dice los comandos exactos de tu asistente:
|
|
441
|
+
|
|
442
|
+
```
|
|
443
|
+
› OpenSpec — los comandos van en el chat del asistente, no en la terminal:
|
|
444
|
+
✓ Copilot: /opsx-explore · /opsx-propose · /opsx-apply · /opsx-archive
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
Si dice que no hay comandos, falta inicializar OpenSpec:
|
|
448
|
+
|
|
449
|
+
```powershell
|
|
450
|
+
openspec init --tools github-copilot --force
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
Después **reinicia VS Code**: los comandos nuevos no aparecen hasta que se recarga.
|
|
454
|
+
|
|
455
|
+
Con el nombre correcto el agente frena al terminar la propuesta y espera tu aprobación antes
|
|
456
|
+
de implementar.
|
|
457
|
+
|
|
458
|
+
Si aun así se pasa de largo —o si te nombra comandos con dos puntos que no existen—, mira la
|
|
459
|
+
versión de OpenSpec: `dai doctor` te avisa si está vieja. Hasta la **1.10.0**, los prompts que
|
|
460
|
+
OpenSpec generaba para Copilot pedían herramientas que Copilot no tiene, y su única forma de
|
|
461
|
+
frenar a preguntarte no existía. Actualiza:
|
|
462
|
+
|
|
463
|
+
```powershell
|
|
464
|
+
npm i -g @fission-ai/openspec@latest
|
|
465
|
+
openspec init --tools github-copilot --force
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
En cualquier caso, **la aprobación del diseño es tuya**: si ves que arrancó a programar sin
|
|
469
|
+
que hayas dicho que sí, páralo y pídele la propuesta.
|
|
470
|
+
|
|
391
471
|
### `dai link-us` dice que no encontró la US en jira
|
|
392
472
|
|
|
393
473
|
Por orden de frecuencia:
|
|
@@ -455,6 +535,26 @@ $env:NODE_EXTRA_CA_CERTS="C:\ruta\ca-empresa.pem"
|
|
|
455
535
|
> lo proponga un asistente. Eso no arregla nada: **apaga la verificación entera**, y por esa
|
|
456
536
|
> conexión viaja tu token de Jira.
|
|
457
537
|
|
|
538
|
+
### `dai check` imprime el ✅ y después tira `Assertion failed … UV_HANDLE_CLOSING`
|
|
539
|
+
|
|
540
|
+
```
|
|
541
|
+
✅ PROJ-125 al día (v1)
|
|
542
|
+
Assertion failed: !(handle->flags & UV_HANDLE_CLOSING), file src\win\async.c, line 94
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
El resultado que ves es el bueno: el chequeo terminó bien. Lo que revienta después es el
|
|
546
|
+
**cierre del proceso** de Node en Windows, desarmando la conexión que se usó para consultar
|
|
547
|
+
Jira. Es un problema de Node, no de tu configuración ni de tu US.
|
|
548
|
+
|
|
549
|
+
Ya está corregido en dai. Actualiza:
|
|
550
|
+
|
|
551
|
+
```powershell
|
|
552
|
+
dai upgrade
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
Importaba arreglarlo porque el proceso terminaba con un código de error aunque el chequeo
|
|
556
|
+
hubiera pasado: un `dai check` verde se reportaba rojo en el CI o en un hook de git.
|
|
557
|
+
|
|
458
558
|
### El CI falla con "falta el link"
|
|
459
559
|
|
|
460
560
|
Es el gate de [`governance/ci-rules.md`](https://github.com/dforce2055/dai/blob/main/governance/ci-rules.md),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dforce2055/dai",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.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/",
|
package/skills/link-us/SKILL.md
CHANGED
|
@@ -25,7 +25,7 @@ Es la contraparte técnica de `grill-user-story`: donde esa produce el QUÉ (en
|
|
|
25
25
|
- Nombre: `feature/ABC-###-<slug>` donde `<slug>` sale del título (minúsculas, sin acentos, `-` como separador).
|
|
26
26
|
- Base según convención del repo (`main` o `develop`). Verificar que la branch no exista ya.
|
|
27
27
|
- No permitir crear la branch si el key no fue validado en el paso 1.
|
|
28
|
-
4. **Generar el link.** Crear `openspec/changes/<change-id>/implements.yaml` a partir de [templates/implements.yaml](templates/implements.yaml), completando `id`, `version`, `ac_hash`, `repo` y `autor`. Dejar `introduces`
|
|
28
|
+
4. **Generar el link.** Crear `openspec/changes/<change-id>/implements.yaml` a partir de [templates/implements.yaml](templates/implements.yaml), completando `id`, `version`, `ac_hash`, `repo` y `autor`. Dejar `introduces` con el placeholder: **no se completa ahora**. Al arrancar no se sabe qué capacidades técnicas va a introducir el change — eso se sabe al terminar de implementarlo, y ahí lo completa quien implementó (agente o dev), con el dev revisándolo en la PR.
|
|
29
29
|
5. **Hand-off.** Ofrecer seguir con `opsx:explore` → `opsx:propose` para armar el change (proposal/design/tasks) sobre la branch ya creada y linkeada.
|
|
30
30
|
|
|
31
31
|
## Guardrails (por qué esta skill existe)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# Link QUÉ↔CÓMO · lo genera link-us / `dai link-us`, lo versiona git junto al código.
|
|
2
|
-
# Es el ÚNICO link
|
|
2
|
+
# Es el ÚNICO link AUTORADO (se escribe, no se deriva): lo scaffoldea `link-us` y se cierra
|
|
3
|
+
# al terminar de implementar. La cobertura inversa la deriva `dai stamp`.
|
|
3
4
|
# Schema: docs/adr/0004-ubicacion-y-schema-implements.md
|
|
4
5
|
|
|
5
6
|
change: <change-id> # ← identidad del CÓMO (nombre local del change/spec). Auto-contenido.
|
|
@@ -11,6 +12,7 @@ implements: # FORWARD: qué QUÉ del negocio cumple este ch
|
|
|
11
12
|
ac_hash: <autogenerado> # ← lo calcula `dai ac-hash`. Dispara el ⚠️ si el QUÉ cambia.
|
|
12
13
|
|
|
13
14
|
introduces: # capacidades TÉCNICAS nuevas que agrega este change (opcional)
|
|
14
|
-
- <capacidad-tecnica> # p. ej. guard-carrito-vacio
|
|
15
|
+
- <capacidad-tecnica> # p. ej. guard-carrito-vacio. Se completa AL TERMINAR de
|
|
16
|
+
# implementar, no al crear el link: recién ahí se sabe.
|
|
15
17
|
|
|
16
18
|
autor: <dev> # quién implementa
|
|
@@ -24,6 +24,9 @@
|
|
|
24
24
|
- [ ] Existe `implements.yaml` con `id`, `version` y `ac_hash`. *(Art. 9)*
|
|
25
25
|
- [ ] El **`ac_hash` coincide** con el de la US vigente (no se implementó una versión atrasada). *(Art. 11)*
|
|
26
26
|
- [ ] La rama sigue la convención (`feature/ABC-###-<slug>`) → ver `governance/branch-naming.md`.
|
|
27
|
+
- [ ] **`introduces` está cerrado**: lista las capacidades técnicas que el change agregó, o el
|
|
28
|
+
bloque se borró porque no agregó ninguna. No queda el placeholder `<capacidad-tecnica>`:
|
|
29
|
+
es el único campo que se completa **al terminar**, y sin cerrarlo el link queda a medias.
|
|
27
30
|
|
|
28
31
|
### Revisión
|
|
29
32
|
- [ ] Pasó el **primer pase de IA** (`dai-review`): sin problemas de correctitud.
|
|
@@ -31,7 +34,9 @@
|
|
|
31
34
|
- [ ] Cumple los estándares del repo (lint, tipos, convenciones).
|
|
32
35
|
|
|
33
36
|
### Cierre
|
|
34
|
-
- [ ] El change se promovió (`
|
|
37
|
+
- [ ] El change se promovió (los comandos `apply` → `archive` de OpenSpec) si aplica.
|
|
38
|
+
*(Se escriben `/opsx:apply` en Claude Code y `/opsx-apply` en Copilot y Cursor —
|
|
39
|
+
`dai doctor` te dice cuál usa este repo.)*
|
|
35
40
|
- [ ] El **CI estampó la cobertura** en el gestor (no la escribió una persona). *(Art. 10)*
|
|
36
41
|
- [ ] La US quedó en estado **implementada**.
|
|
37
42
|
|