@dforce2055/dai 0.12.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +86 -0
- package/VERSION +1 -1
- package/cli/dai.mjs +74 -22
- package/cli/lib/bootstrap.mjs +43 -2
- 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,91 @@
|
|
|
3
3
|
Formato basado en [Keep a Changelog](https://keepachangelog.com/). Versionado semver
|
|
4
4
|
(ver `VERSION`).
|
|
5
5
|
|
|
6
|
+
## [0.13.0] — 2026-08-21
|
|
7
|
+
|
|
8
|
+
**Un dev de backend en Windows siguió el tutorial al pie de la letra y el agente se puso a
|
|
9
|
+
programar sin escribir la propuesta. No era Windows ni era su setup: le estábamos diciendo mal
|
|
10
|
+
el nombre del comando.**
|
|
11
|
+
|
|
12
|
+
### Arreglado
|
|
13
|
+
- **Los comandos de OpenSpec se documentaban solo en la forma de Claude Code.** OpenSpec
|
|
14
|
+
genera un archivo distinto por asistente, y el nombre del comando sale del archivo:
|
|
15
|
+
`.claude/commands/opsx/<id>.md` → `/opsx:propose`, pero
|
|
16
|
+
`.github/prompts/opsx-<id>.prompt.md` → `/opsx-propose` (Copilot) y
|
|
17
|
+
`.cursor/commands/opsx-<id>.md` → `/opsx-propose` (Cursor). O sea: **solo Claude usa los
|
|
18
|
+
dos puntos**, y el tutorial de setup del dev —que es el de Windows + Copilot— mostraba los
|
|
19
|
+
dos puntos en los tres pasos.
|
|
20
|
+
El síntoma no se parece en nada a la causa, y ahí está el daño: tipear `/opsx:propose` en
|
|
21
|
+
Copilot **no da error**. No matchea ningún comando, el workflow nunca se carga, y el agente
|
|
22
|
+
toma el texto suelto como una charla — se saltea el gate propose → aprobación → apply y
|
|
23
|
+
arranca a implementar. Se lee como "el bot hace lo que quiere" y se le echa la culpa al
|
|
24
|
+
asistente, al sistema operativo o al método. Es vibe coding servido por la documentación.
|
|
25
|
+
Ahora `dai init` imprime la forma que le toca a **tu** asistente (`opsxHint`), el tutorial
|
|
26
|
+
de Windows lo dice explícito con su propia entrada en *Cuando algo falla*, y la guía del
|
|
27
|
+
dev aclara que la forma con dos puntos es la de Claude.
|
|
28
|
+
- **`dai check` terminaba con código de error en Windows aunque el chequeo pasara**
|
|
29
|
+
(`Assertion failed: !(handle->flags & UV_HANDLE_CLOSING), file src\win\async.c, line 94`).
|
|
30
|
+
Salía con `process.exit()` inmediatamente después del `fetch` al tracker, y en Windows eso
|
|
31
|
+
aborta el proceso mientras undici todavía está desarmando sus handles. Imprimía
|
|
32
|
+
`✅ al día` y devolvía distinto de cero igual: un gate verde reportado como rojo en CI o en
|
|
33
|
+
un hook de git — justo el modo de falla que apaga un gate.
|
|
34
|
+
Ahora el código de salida se fija con `process.exitCode` y el event loop drena solo. Cuesta
|
|
35
|
+
~40 ms (los sockets keep-alive de undici están *unref'd*) y no cambia nada en macOS/Linux.
|
|
36
|
+
El mismo tratamiento va para los `.catch()` del dispatcher de **todos** los comandos que
|
|
37
|
+
salen a la red o spawnean npm (`link-us`, `stamp`, `update-us`, `edit-us`, `forge`,
|
|
38
|
+
`publish`, `pr`/`mr`, `install`, `init`), con `failSoft()`: ahí el comando ya terminó, así
|
|
39
|
+
que cortar de una no aportaba nada y podía tapar el mensaje de error con un stack de C.
|
|
40
|
+
Adentro de un comando `fail()` sigue saliendo de una — ahí sí hay que no volver.
|
|
41
|
+
|
|
42
|
+
### Agregado
|
|
43
|
+
- **Regla nueva en la constitución: *el diseño se aprueba antes de implementar*.** Al terminar
|
|
44
|
+
la propuesta el agente para y pide aprobación explícita; si no tiene una herramienta para
|
|
45
|
+
preguntar, pregunta en texto plano y espera. Vale para los tres asistentes. Es el borde
|
|
46
|
+
QUÉ↔CÓMO, que sí es dominio de dai: sin esa firma, la implementación no tiene contra qué
|
|
47
|
+
revisarse — y el gate no puede depender de que el asistente de turno tenga la herramienta
|
|
48
|
+
correcta. Los repos ya inicializados la reciben con `dai sync`.
|
|
49
|
+
- **`dai doctor` avisa si OpenSpec está por debajo de 1.10.0.** Hasta esa versión, los prompts
|
|
50
|
+
que OpenSpec generaba para Copilot y Cursor nombraban los comandos en la forma de Claude
|
|
51
|
+
(`/opsx:apply`, que ahí no existe) e invocaban `AskUserQuestion`/`TodoWrite`, que solo tiene
|
|
52
|
+
Claude Code. El agente terminaba nombrando comandos inexistentes y salteándose el gate de
|
|
53
|
+
aprobación porque su única forma de preguntar no existía. Está arreglado upstream
|
|
54
|
+
([#727](https://github.com/Fission-AI/OpenSpec/issues/727),
|
|
55
|
+
[#1307](https://github.com/Fission-AI/OpenSpec/issues/1307),
|
|
56
|
+
[#1103](https://github.com/Fission-AI/OpenSpec/issues/1103)), pero un equipo que instaló
|
|
57
|
+
antes se queda con la versión vieja y el síntoma no se parece a la causa.
|
|
58
|
+
- **`dai doctor` reporta los comandos de OpenSpec**, con la forma que le toca a **este** repo:
|
|
59
|
+
`✓ Copilot: /opsx-explore · /opsx-propose · /opsx-apply · /opsx-archive`. Si no están
|
|
60
|
+
generados, dice el comando exacto para generarlos —con el tool id que espera OpenSpec, que
|
|
61
|
+
para Copilot es `github-copilot` y no `copilot`—. Solo mira los asistentes configurados en
|
|
62
|
+
el repo: tener las skills instaladas globalmente no dice nada de los comandos, que son
|
|
63
|
+
archivos versionados.
|
|
64
|
+
|
|
65
|
+
### Cambiado
|
|
66
|
+
- **`introduces` se cierra al TERMINAR de implementar, y lo normal es que lo escriba el
|
|
67
|
+
agente.** El tutorial del dev pedía completarlo justo después de `dai link-us` — cuando
|
|
68
|
+
todavía no se puede saber qué capacidades técnicas va a introducir el change; al empezar
|
|
69
|
+
sería adivinar. Y la skill `link-us` decía explícito "dejar `introduces` para que el dev lo
|
|
70
|
+
liste", cargándole a mano un dato que sabe mejor quien acaba de implementar.
|
|
71
|
+
La frase "el único archivo que se autora **a mano**" empujaba el malentendido: lo que dice
|
|
72
|
+
el ADR-0004 es que es el único registro **autorado** —se escribe, no se deriva—, y `link-us`
|
|
73
|
+
ya resuelve `id`, `version`, `ac_hash`, `change`, `repo` y `autor` por construcción.
|
|
74
|
+
Ahora lo dicen igual el archivo generado, la skill, su template, el tutorial (con el paso de
|
|
75
|
+
cierre después de `/opsx-apply`), la guía del dev, el glosario y el ejemplo end-to-end. El
|
|
76
|
+
**DoD suma el ítem**: `introduces` cerrado, sin el placeholder `<capacidad-tecnica>`.
|
|
77
|
+
- **El DoD nombra los comandos de OpenSpec sin atarlos a un asistente.** Decía
|
|
78
|
+
`opsx:apply` → `opsx:archive`, la forma de Claude, en un template que viaja a los tres.
|
|
79
|
+
- **`DAI_JIRA_FIELDS_FILE` sale comentada en el `.env.dai` que genera `dai init`.** Apuntaba
|
|
80
|
+
al mismo valor que ya usa el CLI por defecto, así que no aportaba nada — pero hacía creer
|
|
81
|
+
que faltaba un archivo obligatorio, y un dev terminó pidiendo los `customfield_*` de la
|
|
82
|
+
empresa para un archivo que **solo hace falta para crear** US, no para leerlas.
|
|
83
|
+
|
|
84
|
+
### Interno
|
|
85
|
+
- **325 tests** (+6 desde 0.12.0): `opsxCommand` y `opsxHint` —que solo Claude lleva los dos
|
|
86
|
+
puntos, y la línea combinada de `dai init` cuando el repo configura varios asistentes—, el
|
|
87
|
+
archivo y el tool id de OpenSpec por asistente (`github-copilot`, que no es `copilot`),
|
|
88
|
+
`OPENSPEC_MIN`, que `DAI_JIRA_FIELDS_FILE` salga comentada, y que la regla nueva de la
|
|
89
|
+
constitución llegue a los tres asistentes.
|
|
90
|
+
|
|
6
91
|
## [0.12.0] — 2026-08-13
|
|
7
92
|
|
|
8
93
|
**La PR deja de robarle la US a otro. `dai pr` resolvía el link recorriendo todo el repo y
|
|
@@ -611,6 +696,7 @@ ClickUp y Jira Cloud.
|
|
|
611
696
|
- Tests de las rutas de red (jira/clickup/forge) con `fetch` mockeado. Sin links rotos;
|
|
612
697
|
`files` de npm sin tests ni secretos.
|
|
613
698
|
|
|
699
|
+
[0.13.0]: https://github.com/dforce2055/dai/releases/tag/v0.13.0
|
|
614
700
|
[0.12.0]: https://github.com/dforce2055/dai/releases/tag/v0.12.0
|
|
615
701
|
[0.11.0]: https://github.com/dforce2055/dai/releases/tag/v0.11.0
|
|
616
702
|
[0.10.0]: https://github.com/dforce2055/dai/releases/tag/v0.10.0
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.13.0
|
package/cli/dai.mjs
CHANGED
|
@@ -30,9 +30,9 @@ import { parseFindings, diffPositions, validateFindings, filterFindings, renderF
|
|
|
30
30
|
import { composePrBody, prTitle, forgeTool } from "./lib/pr.mjs";
|
|
31
31
|
import { dirsEqual } from "./lib/fsutil.mjs";
|
|
32
32
|
import { parseFlags, parseAssistants, isAssistantToken, asList } from "./lib/args.mjs";
|
|
33
|
-
import { versionDrift, planUpgrade } from "./lib/semver.mjs";
|
|
33
|
+
import { versionDrift, planUpgrade, compareVersions } from "./lib/semver.mjs";
|
|
34
34
|
import { parseSource } from "./lib/skills-source.mjs";
|
|
35
|
-
import { skillToCursor, validateSkill, constitution, constitutionCursorRule, envFor, mergeEnv, upsertBlock, reconcileGitignore, stalePromptFiles } from "./lib/bootstrap.mjs";
|
|
35
|
+
import { skillToCursor, validateSkill, constitution, constitutionCursorRule, envFor, mergeEnv, upsertBlock, reconcileGitignore, stalePromptFiles, opsxHint, opsxCommand, OPSX_COMMAND_FILE, OPENSPEC_TOOL, OPENSPEC_MIN } from "./lib/bootstrap.mjs";
|
|
36
36
|
import { parseFieldsFile, parseFieldOverrides, resolveJiraFields } from "./lib/jira-fields.mjs";
|
|
37
37
|
import { assertProjectKey } from "./lib/pm-jira.mjs";
|
|
38
38
|
import { flattenImplements, stampScope, prScope, matchBranchToImplements, requiresLink, trackerKeysIn } from "./lib/branch-scope.mjs";
|
|
@@ -45,6 +45,13 @@ const HERE = dirname(fileURLToPath(import.meta.url));
|
|
|
45
45
|
process.stdout.on("error", (e) => { if (e.code === "EPIPE") process.exit(0); throw e; });
|
|
46
46
|
|
|
47
47
|
function fail(msg, code = 1) { process.stderr.write("dai: " + msg + "\n"); process.exit(code); }
|
|
48
|
+
|
|
49
|
+
// Igual que fail(), pero sin process.exit(). Lo usan los `.catch()` del dispatcher: ahí el
|
|
50
|
+
// comando YA terminó, así que cortar de una no aporta nada y en Windows cuesta caro — salir
|
|
51
|
+
// de golpe después de un fetch (o de un spawn de npm) aborta el proceso con un assert de
|
|
52
|
+
// libuv y tapa el mensaje de error con un stack de C. Ver la nota en cmdCheckCi.
|
|
53
|
+
// Adentro de un comando `fail()` sigue siendo lo correcto: ahí sí hay que no volver.
|
|
54
|
+
function failSoft(msg, code = 1) { process.stderr.write("dai: " + msg + "\n"); process.exitCode = code; }
|
|
48
55
|
const ok = (m) => process.stdout.write(`✓ ${m}\n`);
|
|
49
56
|
const info = (m) => process.stdout.write(`› ${m}\n`);
|
|
50
57
|
const warn = (m) => process.stdout.write(`⚠ ${m}\n`);
|
|
@@ -219,6 +226,13 @@ function gitCommit() { try { return git(["rev-parse", "HEAD"]); } catch { return
|
|
|
219
226
|
// Quién decide es requiresLink(), leyendo el nombre de la branch.
|
|
220
227
|
//
|
|
221
228
|
// Salidas: 0 = pasa · 1 = falta el link · 2 = hay link pero el QUÉ cambió (atrasado)
|
|
229
|
+
//
|
|
230
|
+
// El código de salida se fija con `process.exitCode`, NO con `process.exit()`. En Windows,
|
|
231
|
+
// salir de golpe justo después de un fetch aborta el proceso con un assert de libuv
|
|
232
|
+
// (`!(handle->flags & UV_HANDLE_CLOSING)`, src\win\async.c) mientras undici todavía está
|
|
233
|
+
// desarmando sus handles. El gate imprimía "al día" y devolvía distinto de cero igual: un
|
|
234
|
+
// verde reportado como rojo en CI o en un hook. Dejar que el loop drene solo cuesta ~40ms
|
|
235
|
+
// (los sockets keep-alive de undici están unref'd) y no cambia nada en macOS/Linux.
|
|
222
236
|
async function cmdCheckCi(opts = {}) {
|
|
223
237
|
const branch = opts.branch || process.env.DAI_CI_BRANCH || ciBranch() || gitBranch();
|
|
224
238
|
const { required, reason } = requiresLink(branch);
|
|
@@ -227,7 +241,7 @@ async function cmdCheckCi(opts = {}) {
|
|
|
227
241
|
info(`branch '${branch || "(desconocida)"}' — ${reason}`);
|
|
228
242
|
if (!required) {
|
|
229
243
|
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.");
|
|
244
|
+
else { ok("gate OK — esta branch no requiere US."); return; }
|
|
231
245
|
}
|
|
232
246
|
if (required && rows.length === 0) {
|
|
233
247
|
const ids = trackerKeysIn(branch);
|
|
@@ -236,7 +250,7 @@ async function cmdCheckCi(opts = {}) {
|
|
|
236
250
|
` Crealo: dai link-us ${ids[0] || "<ID-DE-LA-US>"}\n` +
|
|
237
251
|
" Si NO implementa una US (tooling, deps, docs), renombrá la branch con un\n" +
|
|
238
252
|
" prefijo exento — chore/, docs/, ci/ — según governance/branch-naming.md.\n");
|
|
239
|
-
process.
|
|
253
|
+
process.exitCode = 1; return;
|
|
240
254
|
}
|
|
241
255
|
|
|
242
256
|
// Hay link: que además esté al día contra la US viva. Sin token/backend eso no se
|
|
@@ -244,7 +258,7 @@ async function cmdCheckCi(opts = {}) {
|
|
|
244
258
|
// con --no-network (o sin adaptador utilizable) valida el link y no más.
|
|
245
259
|
if (opts.noNetwork) {
|
|
246
260
|
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.
|
|
261
|
+
process.exitCode = 0; return;
|
|
248
262
|
}
|
|
249
263
|
loadDaiEnv();
|
|
250
264
|
let adapter;
|
|
@@ -252,7 +266,7 @@ async function cmdCheckCi(opts = {}) {
|
|
|
252
266
|
catch (e) {
|
|
253
267
|
warn(`no puedo comparar contra la US viva: ${e.message}`);
|
|
254
268
|
ok(`gate OK igual — el link existe (${rows.map((r) => r.id).join(", ")}). Configurá el backend para chequear también el atraso.`);
|
|
255
|
-
process.
|
|
269
|
+
process.exitCode = 0; return;
|
|
256
270
|
}
|
|
257
271
|
let worst = 0;
|
|
258
272
|
for (const r of rows) {
|
|
@@ -271,7 +285,7 @@ async function cmdCheckCi(opts = {}) {
|
|
|
271
285
|
}
|
|
272
286
|
}
|
|
273
287
|
if (worst === 0) ok(`gate OK — ${rows.length} US linkeada(s) y al día.`);
|
|
274
|
-
process.
|
|
288
|
+
process.exitCode = worst;
|
|
275
289
|
}
|
|
276
290
|
|
|
277
291
|
// La branch real en CI: en una PR, HEAD es un merge commit detached, así que
|
|
@@ -285,6 +299,7 @@ function ciBranch() {
|
|
|
285
299
|
}
|
|
286
300
|
|
|
287
301
|
// ── check ──────────────────────────────────────────────────────────────────
|
|
302
|
+
// `process.exitCode` en vez de `process.exit()`: ver la nota en cmdCheckCi.
|
|
288
303
|
async function cmdCheck() {
|
|
289
304
|
loadDaiEnv();
|
|
290
305
|
const adapter = getAdapter(process.env);
|
|
@@ -312,7 +327,7 @@ async function cmdCheck() {
|
|
|
312
327
|
for (const id of atrasadas) process.stdout.write(` dai link-us ${id} --resync # re-estampa el ac_hash contra la US viva\n`);
|
|
313
328
|
process.stdout.write(" Después, revisa si tu implementación cubre el criterio nuevo.\n");
|
|
314
329
|
}
|
|
315
|
-
process.
|
|
330
|
+
process.exitCode = worst;
|
|
316
331
|
}
|
|
317
332
|
|
|
318
333
|
// ── stamp ──────────────────────────────────────────────────────────────────
|
|
@@ -1263,7 +1278,7 @@ async function cmdInit(repo, opts) {
|
|
|
1263
1278
|
let installOpenspec = opts.openspec === true;
|
|
1264
1279
|
if (!hasOpenspec && opts.openspec === undefined && rl) {
|
|
1265
1280
|
process.stdout.write("\n OpenSpec es el motor recomendado del CÓMO: convierte la US en design + tasks\n");
|
|
1266
|
-
process.stdout.write(
|
|
1281
|
+
process.stdout.write(` (${opsxHint(want)}). No está en este repo — la trazabilidad de dai anda igual sin\n`);
|
|
1267
1282
|
process.stdout.write(" él, pero para el flujo completo conviene tenerlo.\n");
|
|
1268
1283
|
installOpenspec = await askYesNo(rl, "¿Instalar el CLI de OpenSpec ahora? (después ejecutas `openspec init` tú)", false);
|
|
1269
1284
|
}
|
|
@@ -1366,7 +1381,7 @@ async function cmdInit(repo, opts) {
|
|
|
1366
1381
|
.filter(Boolean).join(",") || "claude,github-copilot,cursor";
|
|
1367
1382
|
const osHint = "para sumarlo después: npm i -g @fission-ai/openspec@latest && openspec init --tools " + osTools;
|
|
1368
1383
|
if (hasOpenspec) {
|
|
1369
|
-
ok(
|
|
1384
|
+
ok(`OpenSpec: ya inicializado en el repo — ${opsxHint(want)}`);
|
|
1370
1385
|
} else if (openspecPartial) {
|
|
1371
1386
|
warn("OpenSpec: hay una carpeta openspec/ a medias. Reinicializa: rm -rf openspec && openspec init --tools " + osTools + " --force");
|
|
1372
1387
|
} else if (installOpenspec) {
|
|
@@ -1379,7 +1394,7 @@ async function cmdInit(repo, opts) {
|
|
|
1379
1394
|
try {
|
|
1380
1395
|
info(`OpenSpec: inicializando en el repo (--tools ${osTools})…`);
|
|
1381
1396
|
runNpmTool("openspec", ["init", "--tools", osTools, "--force"], { stdio: "inherit", cwd: repo === "." ? process.cwd() : repo });
|
|
1382
|
-
ok(
|
|
1397
|
+
ok(`OpenSpec: instalado e inicializado — genera design/tasks con ${opsxHint(want)}`);
|
|
1383
1398
|
} catch {
|
|
1384
1399
|
warn("OpenSpec: el CLI está pero falló `openspec init`. Ejecuta a mano en el repo:");
|
|
1385
1400
|
process.stdout.write(` openspec init --tools ${osTools} --force\n`);
|
|
@@ -1645,6 +1660,43 @@ function cmdDoctor() {
|
|
|
1645
1660
|
else warn("falta constitución Cursor (dai-constitution.mdc)");
|
|
1646
1661
|
}
|
|
1647
1662
|
|
|
1663
|
+
// OpenSpec: los comandos que arman el CÓMO. Se reportan acá porque su nombre CAMBIA
|
|
1664
|
+
// según el asistente, y con la forma equivocada el agente no falla — no encuentra nada,
|
|
1665
|
+
// no avisa, y se pone a improvisar salteándose el gate. Verlo escrito ahorra la tarde.
|
|
1666
|
+
const OPSX_IDS = ["explore", "propose", "apply", "archive"];
|
|
1667
|
+
const opsxPath = (kind, id) => join(cwd, ...OPSX_COMMAND_FILE[kind].replace("<id>", id).split("/"));
|
|
1668
|
+
// Solo los asistentes configurados EN ESTE REPO: los comandos opsx son archivos del
|
|
1669
|
+
// repo, así que tener las skills globales no dice nada sobre ellos. Avisar por los otros
|
|
1670
|
+
// dos le pone dos warnings falsos a quien configuró uno solo, que es el caso normal.
|
|
1671
|
+
const localActive = ASSISTANTS.filter((a) => skillNames.some((n) => existsSync(join(a.local, n))));
|
|
1672
|
+
if (localActive.length && existsSync(join(cwd, "openspec"))) {
|
|
1673
|
+
info("OpenSpec — los comandos van en el chat del asistente, no en la terminal:");
|
|
1674
|
+
for (const a of localActive) {
|
|
1675
|
+
const faltan = OPSX_IDS.filter((id) => !existsSync(opsxPath(a.kind, id)));
|
|
1676
|
+
if (faltan.length === OPSX_IDS.length) {
|
|
1677
|
+
warn(`${a.label}: no hay comandos opsx. Generalos: openspec init --tools ${OPENSPEC_TOOL[a.kind]} --force`);
|
|
1678
|
+
} else {
|
|
1679
|
+
const hay = OPSX_IDS.filter((id) => existsSync(opsxPath(a.kind, id)));
|
|
1680
|
+
ok(`${a.label}: ${hay.map((id) => opsxCommand(a.kind, id)).join(" · ")}`);
|
|
1681
|
+
if (faltan.length) warn(`${a.label}: faltan ${faltan.join(", ")} — regenerá con \`openspec init --tools ${OPENSPEC_TOOL[a.kind]} --force\``);
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
process.stdout.write(" (el nombre sale del archivo que genera OpenSpec: solo Claude usa los dos puntos)\n");
|
|
1685
|
+
// La versión importa para algo más que features: hasta OPENSPEC_MIN, los prompts que
|
|
1686
|
+
// OpenSpec genera para Copilot/Cursor decían `/opsx:apply` (la forma de Claude, que ahí
|
|
1687
|
+
// no existe) e invocaban herramientas que solo tiene Claude Code. El agente terminaba
|
|
1688
|
+
// nombrando comandos inexistentes y salteándose el gate de aprobación sin avisar.
|
|
1689
|
+
let osV = null;
|
|
1690
|
+
try { osV = String(runNpmTool("openspec", ["--version"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }) ?? "").trim(); } catch { /* sin CLI */ }
|
|
1691
|
+
if (!osV) info("el CLI de OpenSpec no está en el PATH (los comandos igual viven en el repo)");
|
|
1692
|
+
else if (compareVersions(osV, OPENSPEC_MIN) === -1) {
|
|
1693
|
+
warn(`OpenSpec ${osV} — actualiza a ${OPENSPEC_MIN} o mayor: npm i -g @fission-ai/openspec@latest`);
|
|
1694
|
+
process.stdout.write(" Hasta esa versión, los prompts de Copilot y Cursor nombraban los comandos en la\n");
|
|
1695
|
+
process.stdout.write(" forma de Claude y pedían herramientas que esos asistentes no tienen: el agente se\n");
|
|
1696
|
+
process.stdout.write(" saltea el gate de aprobación y se pone a implementar sin avisar.\n");
|
|
1697
|
+
} else ok(`OpenSpec ${osV}`);
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1648
1700
|
info("adaptador de PM:");
|
|
1649
1701
|
const pm = process.env.DAI_PM || "md";
|
|
1650
1702
|
ok(`DAI_PM=${pm}`);
|
|
@@ -1693,23 +1745,23 @@ const { opts, pos } = parseFlags(rest);
|
|
|
1693
1745
|
switch (cmd) {
|
|
1694
1746
|
case "ac-hash": cmdAcHash(pos[0]); break;
|
|
1695
1747
|
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) =>
|
|
1748
|
+
case "link-us": cmdLinkUs(pos[0], opts).catch((e) => failSoft(String(e.message))); break;
|
|
1749
|
+
case "check": (opts.ci ? cmdCheckCi(opts) : cmdCheck()).catch((e) => failSoft(String(e.message))); break;
|
|
1750
|
+
case "stamp": cmdStamp(pos, opts).catch((e) => failSoft(String(e.message))); break;
|
|
1751
|
+
case "update-us": cmdUpdateUs(pos[0], opts).catch((e) => failSoft(String(e.message))); break;
|
|
1752
|
+
case "edit-us": cmdEditUs(pos[0], opts).catch((e) => failSoft(String(e.message))); break;
|
|
1753
|
+
case "forge": cmdForge(pos[0], pos[1], opts).catch((e) => failSoft(String(e.message))); break;
|
|
1754
|
+
case "publish": cmdPublish(pos[0], opts).catch((e) => failSoft(String(e.message))); break;
|
|
1703
1755
|
case "pr":
|
|
1704
|
-
case "mr": cmdPr(opts).catch((e) =>
|
|
1756
|
+
case "mr": cmdPr(opts).catch((e) => failSoft(String(e.message))); break; // `mr` = alias para GitLab (merge request)
|
|
1705
1757
|
case "done": cmdDone(opts); break;
|
|
1706
1758
|
case "archive": cmdArchive(pos[0], opts); break;
|
|
1707
|
-
case "install": cmdInstall(opts).catch((e) =>
|
|
1759
|
+
case "install": cmdInstall(opts).catch((e) => failSoft(String(e.message))); break; // alias de `dai skills install`
|
|
1708
1760
|
case "skills":
|
|
1709
|
-
if (pos[0] === "install" || pos[0] === undefined) cmdInstall(opts).catch((e) =>
|
|
1761
|
+
if (pos[0] === "install" || pos[0] === undefined) cmdInstall(opts).catch((e) => failSoft(String(e.message)));
|
|
1710
1762
|
else fail(`subcomando de skills desconocido: '${pos[0]}' (por ahora: install)`, 2);
|
|
1711
1763
|
break;
|
|
1712
|
-
case "init": cmdInit(pos[0], opts).catch((e) =>
|
|
1764
|
+
case "init": cmdInit(pos[0], opts).catch((e) => failSoft(String(e.message))); break;
|
|
1713
1765
|
case "sync": cmdSync(pos[0], opts); break;
|
|
1714
1766
|
case "upgrade":
|
|
1715
1767
|
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.
|
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.0",
|
|
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
|
|