@dforce2055/dai 0.7.0 → 0.8.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/.env.example +9 -3
- package/CHANGELOG.md +65 -0
- package/README.md +12 -6
- package/VERSION +1 -1
- package/cli/dai.mjs +127 -49
- package/cli/lib/args.mjs +13 -2
- package/cli/lib/bootstrap.mjs +69 -19
- package/cli/lib/http.mjs +61 -0
- package/cli/lib/jira-fields.mjs +143 -0
- package/cli/lib/pm-jira.mjs +52 -12
- package/docs/adr/0014-copilot-agent-skills.md +99 -0
- package/docs/adr/0015-jira-corporativo.md +104 -0
- package/docs/adr/README.md +4 -0
- package/package.json +1 -1
- package/skills/dai-review/SKILL.md +1 -1
- package/skills/doc-to-backlog/SKILL.md +1 -1
- package/skills/grill-epic/SKILL.md +13 -5
- package/skills/grill-intent/SKILL.md +1 -1
- package/skills/grill-user-story/SKILL.md +16 -3
- package/skills/link-us/SKILL.md +1 -1
- package/skills/tdd/SKILL.md +1 -1
- package/templates/jira-fields.example.json +49 -0
- package/templates/skill.md +8 -4
package/cli/lib/http.mjs
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// dai · fetch con diagnóstico de TLS (cero dependencias).
|
|
2
|
+
//
|
|
3
|
+
// En una red corporativa el proxy suele interceptar TLS con una CA propia. Node NO usa
|
|
4
|
+
// el trust store del sistema (trae su propia lista de CAs), así que falla justo donde el
|
|
5
|
+
// navegador anda — y el error crudo de undici no dice nada de eso.
|
|
6
|
+
//
|
|
7
|
+
// El fix es DECLARAR la CA (NODE_EXTRA_CA_CERTS), nunca apagar la verificación:
|
|
8
|
+
// NODE_TLS_REJECT_UNAUTHORIZED=0 hace que la conexión acepte CUALQUIER certificado, y
|
|
9
|
+
// por acá viajan los tokens del tracker. Este módulo existe para que el que se topa con
|
|
10
|
+
// el muro encuentre el camino correcto antes que el atajo peligroso.
|
|
11
|
+
|
|
12
|
+
const TLS_CODES = new Set([
|
|
13
|
+
"UNABLE_TO_VERIFY_LEAF_SIGNATURE",
|
|
14
|
+
"UNABLE_TO_GET_ISSUER_CERT_LOCALLY",
|
|
15
|
+
"SELF_SIGNED_CERT_IN_CHAIN",
|
|
16
|
+
"DEPTH_ZERO_SELF_SIGNED_CERT",
|
|
17
|
+
"CERT_HAS_EXPIRED",
|
|
18
|
+
"ERR_TLS_CERT_ALTNAME_INVALID",
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
// Extrae el código de error TLS de un fallo de fetch, o null si no es de TLS.
|
|
22
|
+
// undici anida el error real en `cause` (a veces dos niveles).
|
|
23
|
+
export function tlsErrorCode(e) {
|
|
24
|
+
for (let cur = e, depth = 0; cur && depth < 4; cur = cur.cause, depth++) {
|
|
25
|
+
const code = cur.code;
|
|
26
|
+
if (code && TLS_CODES.has(code)) return code;
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function tlsHint(host, code) {
|
|
32
|
+
return [
|
|
33
|
+
`no pude verificar el certificado TLS de ${host} (${code}).`,
|
|
34
|
+
"",
|
|
35
|
+
" Casi siempre es un proxy corporativo que intercepta TLS con su propia CA. Node no",
|
|
36
|
+
" usa el trust store de Windows/macOS, por eso el navegador anda y dai no.",
|
|
37
|
+
"",
|
|
38
|
+
" Solución: exportá la CA raíz de tu empresa a un .pem y declarásela a Node:",
|
|
39
|
+
" Windows (PowerShell): $env:NODE_EXTRA_CA_CERTS=\"C:\\ruta\\ca-empresa.pem\"",
|
|
40
|
+
" macOS / Linux: export NODE_EXTRA_CA_CERTS=/ruta/ca-empresa.pem",
|
|
41
|
+
"",
|
|
42
|
+
" NO uses NODE_TLS_REJECT_UNAUTHORIZED=0. Eso no arregla nada: apaga la verificación",
|
|
43
|
+
" entera, y tu token del tracker viajaría aceptando cualquier certificado — incluido",
|
|
44
|
+
" el de alguien haciéndose pasar por el tracker.",
|
|
45
|
+
].join("\n");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// fetch, pero traduciendo el fallo de TLS a un error accionable.
|
|
49
|
+
export async function daiFetch(url, init) {
|
|
50
|
+
try {
|
|
51
|
+
return await fetch(url, init);
|
|
52
|
+
} catch (e) {
|
|
53
|
+
const code = tlsErrorCode(e);
|
|
54
|
+
if (!code) throw e;
|
|
55
|
+
let host = String(url);
|
|
56
|
+
try { host = new URL(url).host; } catch { /* url rara: mostramos lo que vino */ }
|
|
57
|
+
const err = new Error(tlsHint(host, code));
|
|
58
|
+
err.cause = e;
|
|
59
|
+
throw err;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// dai · campos obligatorios propios de Jira, declarados por issuetype (cero dependencias).
|
|
2
|
+
//
|
|
3
|
+
// El Jira de una empresa casi siempre exige campos propios (customfield_NNNNN) que el
|
|
4
|
+
// alta de un issue rechaza si faltan. Y su valor no es config fija: "Clasificación" es
|
|
5
|
+
// Mejora o Corrección SEGÚN LA US. Un default fijo publicaría todas iguales — un dato
|
|
6
|
+
// incorrecto en Jira, en silencio, que es peor que fallar.
|
|
7
|
+
//
|
|
8
|
+
// Así que se declaran acá, con nombre humano, forma y opciones válidas, y dai valida
|
|
9
|
+
// ANTES de llamar a la API: un typo da un error local con la lista, no un 400 críptico.
|
|
10
|
+
//
|
|
11
|
+
// .dai/jira-fields.json:
|
|
12
|
+
// {
|
|
13
|
+
// "Story": {
|
|
14
|
+
// "clasificacion": {
|
|
15
|
+
// "field": "customfield_10042", // el id de Jira (requerido)
|
|
16
|
+
// "shape": "select", // opcional — ver SHAPES
|
|
17
|
+
// "default": "Mejora", // opcional — si falta, --field es obligatorio
|
|
18
|
+
// "options": ["Mejora", "Corrección"] // opcional — si está, dai valida contra ella
|
|
19
|
+
// }
|
|
20
|
+
// },
|
|
21
|
+
// "Epic": { … }
|
|
22
|
+
// }
|
|
23
|
+
//
|
|
24
|
+
// Un campo DECLARADO tiene que resolver a un valor (default o --field): se declara porque
|
|
25
|
+
// Jira lo exige. Si es opcional, no lo declares.
|
|
26
|
+
|
|
27
|
+
// Cómo se envuelve el valor para la API de Jira:
|
|
28
|
+
// select → { "value": X } el caso típico de un desplegable
|
|
29
|
+
// text → X texto plano
|
|
30
|
+
// multi → [{ "value": X }, …] lista (el valor se parte por comas)
|
|
31
|
+
// raw → JSON.parse(X) escape hatch: cualquier forma que Jira pida
|
|
32
|
+
const SHAPES = new Set(["select", "text", "multi", "raw"]);
|
|
33
|
+
|
|
34
|
+
// JSON no admite comentarios, y este archivo lo edita gente que no escribe código. Las
|
|
35
|
+
// claves que empiezan con "_" son notas y se ignoran — así el molde se explica solo.
|
|
36
|
+
const isNote = (k) => k.startsWith("_");
|
|
37
|
+
|
|
38
|
+
// Si no se declara `shape`: con options es un desplegable; sin options, texto.
|
|
39
|
+
const shapeOf = (def) => def.shape || (Array.isArray(def.options) ? "select" : "text");
|
|
40
|
+
|
|
41
|
+
const optionsHint = (def) => (Array.isArray(def.options) && def.options.length ? `\n válidas: ${def.options.join(" | ")}` : "");
|
|
42
|
+
|
|
43
|
+
// Valida la estructura del archivo y lo devuelve. Los errores nombran la ruta exacta
|
|
44
|
+
// (`Story.clasificacion`) para que se arregle sin adivinar.
|
|
45
|
+
export function parseFieldsFile(text, path = ".dai/jira-fields.json") {
|
|
46
|
+
let json;
|
|
47
|
+
try { json = JSON.parse(text); }
|
|
48
|
+
catch (e) { throw new Error(`${path} no es JSON válido: ${e.message}`); }
|
|
49
|
+
if (json === null || typeof json !== "object" || Array.isArray(json)) {
|
|
50
|
+
throw new Error(`${path}: se espera un objeto { "<issuetype>": { "<alias>": {…} } }`);
|
|
51
|
+
}
|
|
52
|
+
for (const [type, entry] of Object.entries(json)) {
|
|
53
|
+
if (isNote(type)) continue; // JSON no tiene comentarios: las claves _x son notas
|
|
54
|
+
if (entry === null || typeof entry !== "object" || Array.isArray(entry)) {
|
|
55
|
+
throw new Error(`${path}: '${type}' tiene que ser un objeto de campos`);
|
|
56
|
+
}
|
|
57
|
+
for (const [alias, def] of Object.entries(entry)) {
|
|
58
|
+
if (isNote(alias)) continue;
|
|
59
|
+
const at = `${path}: ${type}.${alias}`;
|
|
60
|
+
if (def === null || typeof def !== "object" || Array.isArray(def)) throw new Error(`${at} tiene que ser un objeto`);
|
|
61
|
+
if (typeof def.field !== "string" || !def.field.trim()) {
|
|
62
|
+
throw new Error(`${at}: falta 'field' (el id de Jira, p. ej. "customfield_10042")`);
|
|
63
|
+
}
|
|
64
|
+
if (def.shape !== undefined && !SHAPES.has(def.shape)) {
|
|
65
|
+
throw new Error(`${at}: shape '${def.shape}' desconocido (${[...SHAPES].join(" | ")})`);
|
|
66
|
+
}
|
|
67
|
+
if (def.options !== undefined && !Array.isArray(def.options)) {
|
|
68
|
+
throw new Error(`${at}: 'options' tiene que ser una lista`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return json;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// `--field alias=valor` (repetible) → { alias: "valor" }.
|
|
76
|
+
export function parseFieldOverrides(input) {
|
|
77
|
+
const list = input === undefined ? [] : Array.isArray(input) ? input : [input];
|
|
78
|
+
const out = {};
|
|
79
|
+
for (const raw of list) {
|
|
80
|
+
if (typeof raw !== "string") throw new Error("--field espera 'alias=valor'");
|
|
81
|
+
const i = raw.indexOf("=");
|
|
82
|
+
if (i <= 0) throw new Error(`--field espera 'alias=valor' (recibí: '${raw}')`);
|
|
83
|
+
out[raw.slice(0, i).trim()] = raw.slice(i + 1);
|
|
84
|
+
}
|
|
85
|
+
return out;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Los campos declarados para un issuetype. Exacto primero, después sin distinguir
|
|
89
|
+
// mayúsculas ("story" encuentra "Story"), y {} si no hay nada declarado.
|
|
90
|
+
function specFor(spec, issuetype) {
|
|
91
|
+
if (!spec || typeof spec !== "object") return {};
|
|
92
|
+
const lower = String(issuetype).toLowerCase();
|
|
93
|
+
const hit = Object.keys(spec).find((k) => !isNote(k) && (k === issuetype || k.toLowerCase() === lower));
|
|
94
|
+
if (!hit) return {};
|
|
95
|
+
return Object.fromEntries(Object.entries(spec[hit]).filter(([alias]) => !isNote(alias)));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function checkOptions(def, alias, value) {
|
|
99
|
+
if (!Array.isArray(def.options) || def.options.length === 0) return;
|
|
100
|
+
const values = shapeOf(def) === "multi" ? value.split(",").map((s) => s.trim()).filter(Boolean) : [value];
|
|
101
|
+
for (const v of values) {
|
|
102
|
+
if (!def.options.includes(v)) throw new Error(`'${v}' no es opción de '${alias}'.${optionsHint(def)}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function castValue(def, alias, value) {
|
|
107
|
+
switch (shapeOf(def)) {
|
|
108
|
+
case "select": return { value };
|
|
109
|
+
case "text": return value;
|
|
110
|
+
case "multi": return value.split(",").map((s) => s.trim()).filter(Boolean).map((v) => ({ value: v }));
|
|
111
|
+
case "raw":
|
|
112
|
+
try { return JSON.parse(value); }
|
|
113
|
+
catch (e) { throw new Error(`--field ${alias}: shape 'raw' espera JSON válido — ${e.message}`); }
|
|
114
|
+
default: throw new Error(`shape '${def.shape}' desconocido en '${alias}'`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Resuelve los campos declarados para este issuetype al payload que espera la API.
|
|
119
|
+
// Lanza (sin tocar la red) si un override no existe, si falta un valor obligatorio,
|
|
120
|
+
// o si el valor no está entre las opciones válidas.
|
|
121
|
+
export function resolveJiraFields({ spec, issuetype, overrides = {} }) {
|
|
122
|
+
const entry = specFor(spec, issuetype);
|
|
123
|
+
const aliases = Object.keys(entry);
|
|
124
|
+
|
|
125
|
+
for (const a of Object.keys(overrides)) {
|
|
126
|
+
if (Object.prototype.hasOwnProperty.call(entry, a)) continue;
|
|
127
|
+
throw new Error(aliases.length
|
|
128
|
+
? `--field '${a}' no está declarado para el issuetype '${issuetype}'.\n declarados: ${aliases.join(" | ")}`
|
|
129
|
+
: `--field '${a}': no hay campos declarados para el issuetype '${issuetype}' (revisá el archivo de campos).`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const out = {};
|
|
133
|
+
for (const [alias, def] of Object.entries(entry)) {
|
|
134
|
+
const raw = Object.prototype.hasOwnProperty.call(overrides, alias) ? overrides[alias] : def.default;
|
|
135
|
+
if (raw === undefined) {
|
|
136
|
+
throw new Error(`falta el valor de '${alias}' (lo exige el issuetype '${issuetype}').\n pasalo con: --field ${alias}=<valor>${optionsHint(def)}`);
|
|
137
|
+
}
|
|
138
|
+
const value = String(raw);
|
|
139
|
+
checkOptions(def, alias, value);
|
|
140
|
+
out[def.field] = castValue(def, alias, value);
|
|
141
|
+
}
|
|
142
|
+
return out;
|
|
143
|
+
}
|
package/cli/lib/pm-jira.mjs
CHANGED
|
@@ -7,9 +7,30 @@
|
|
|
7
7
|
// hasher encuentre "## Criterios de aceptación"); al ESCRIBIR el stamp, componemos ADF.
|
|
8
8
|
|
|
9
9
|
import { parseUS } from "./us.mjs";
|
|
10
|
+
import { daiFetch } from "./http.mjs";
|
|
10
11
|
|
|
11
12
|
const trim = (b) => String(b || "").replace(/\/+$/, "");
|
|
12
13
|
|
|
14
|
+
// La clave del PROYECTO (PROJ), no la de un ticket (PROJ-42). Confundirlas es el
|
|
15
|
+
// error de config más común: `dai init` deja DAI_JIRA_PROJECT vacío y quien lo completa
|
|
16
|
+
// suele pegar la épica que tiene a mano. Jira responde un 400 que no lo explica.
|
|
17
|
+
export function assertProjectKey(key) {
|
|
18
|
+
if (!key) throw new Error("falta DAI_JIRA_PROJECT en el .env (la clave del proyecto donde crear el issue).");
|
|
19
|
+
const k = String(key).trim();
|
|
20
|
+
const m = k.match(/^([A-Za-z][A-Za-z0-9_]*)-\d+$/);
|
|
21
|
+
if (m) {
|
|
22
|
+
throw new Error(
|
|
23
|
+
`DAI_JIRA_PROJECT='${k}' es la clave de un ticket, no la del proyecto.\n` +
|
|
24
|
+
` Usá solo la parte de adelante: DAI_JIRA_PROJECT=${m[1]}\n` +
|
|
25
|
+
` Si lo que querías era colgar la US de esa épica: dai publish <us.md> --parent ${k}`,
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
if (!/^[A-Za-z][A-Za-z0-9_]*$/.test(k)) {
|
|
29
|
+
throw new Error(`DAI_JIRA_PROJECT='${k}' no parece una clave de proyecto (letras y números, empezando por letra — p. ej. PROJ).`);
|
|
30
|
+
}
|
|
31
|
+
return k;
|
|
32
|
+
}
|
|
33
|
+
|
|
13
34
|
export function jiraIssueUrl(base, id) {
|
|
14
35
|
return `${trim(base)}/rest/api/3/issue/${encodeURIComponent(id)}?fields=summary,description`;
|
|
15
36
|
}
|
|
@@ -85,37 +106,56 @@ export function renderCoverageAdf(id, r) {
|
|
|
85
106
|
return { type: "doc", version: 1, content };
|
|
86
107
|
}
|
|
87
108
|
|
|
109
|
+
// Un 400 al crear casi siempre es un campo obligatorio del proyecto que no mandamos.
|
|
110
|
+
// El cuerpo de Jira nombra el customfield_NNNNN pero no dice qué hacer: lo decimos acá.
|
|
111
|
+
function createHint(status) {
|
|
112
|
+
if (status !== 400) return "";
|
|
113
|
+
return "\n\n Si el error nombra un 'customfield_NNNNN', tu proyecto exige un campo propio.\n" +
|
|
114
|
+
" Declaralo en .dai/jira-fields.json y volvé a publicar — no hace falta improvisar\n" +
|
|
115
|
+
" una llamada a mano:\n" +
|
|
116
|
+
' { "Story": { "clasificacion": { "field": "customfield_NNNNN",\n' +
|
|
117
|
+
' "options": ["Mejora", "Corrección"] } } }\n' +
|
|
118
|
+
" dai publish <us.md> --field clasificacion=Corrección";
|
|
119
|
+
}
|
|
120
|
+
|
|
88
121
|
export function jiraAdapter(env) {
|
|
89
122
|
const base = env.DAI_JIRA_BASE_URL;
|
|
90
123
|
if (!base) throw new Error("falta DAI_JIRA_BASE_URL en el .env (backend jira).");
|
|
91
124
|
return {
|
|
92
125
|
kind: "jira",
|
|
93
126
|
async fetchUS(id) {
|
|
94
|
-
const res = await
|
|
127
|
+
const res = await daiFetch(jiraIssueUrl(base, id), { headers: jiraAuthHeaders(env) });
|
|
95
128
|
if (res.status === 404) return null;
|
|
96
129
|
if (!res.ok) throw new Error(`jira ${res.status}: ${await res.text()}`);
|
|
97
130
|
return { id, ...parseUS(jiraIssueToText(await res.json())) };
|
|
98
131
|
},
|
|
99
132
|
async stamp(id, record) {
|
|
100
|
-
const res = await
|
|
133
|
+
const res = await daiFetch(jiraCommentUrl(base, id), {
|
|
101
134
|
method: "POST", headers: jiraAuthHeaders(env),
|
|
102
135
|
body: JSON.stringify({ body: renderCoverageAdf(id, record) }),
|
|
103
136
|
});
|
|
104
137
|
if (!res.ok) throw new Error(`jira ${res.status}: ${await res.text()}`);
|
|
105
138
|
return `${trim(base)}/browse/${id}`;
|
|
106
139
|
},
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
const
|
|
140
|
+
// `fields` son los campos propios del proyecto ya resueltos (ver jira-fields.mjs);
|
|
141
|
+
// `parent` cuelga la US de su épica. Ambos son opcionales: un Jira sin campos
|
|
142
|
+
// obligatorios sigue publicando igual que antes.
|
|
143
|
+
async createUS({ title, descriptionMarkdown, parent, issuetype, project, fields }) {
|
|
144
|
+
const key = assertProjectKey(project || env.DAI_JIRA_PROJECT);
|
|
145
|
+
const type = issuetype || env.DAI_JIRA_ISSUETYPE || "Story";
|
|
146
|
+
const payload = {
|
|
147
|
+
project: { key },
|
|
148
|
+
issuetype: { name: type },
|
|
149
|
+
summary: title,
|
|
150
|
+
description: markdownToAdf(descriptionMarkdown),
|
|
151
|
+
...(fields || {}),
|
|
152
|
+
};
|
|
153
|
+
if (parent) payload.parent = { key: String(parent).trim() };
|
|
154
|
+
const res = await daiFetch(`${trim(base)}/rest/api/3/issue`, {
|
|
112
155
|
method: "POST", headers: jiraAuthHeaders(env),
|
|
113
|
-
body: JSON.stringify({ fields:
|
|
114
|
-
project: { key: project }, issuetype: { name: issuetype },
|
|
115
|
-
summary: title, description: markdownToAdf(descriptionMarkdown),
|
|
116
|
-
} }),
|
|
156
|
+
body: JSON.stringify({ fields: payload }),
|
|
117
157
|
});
|
|
118
|
-
if (!res.ok) throw new Error(`jira ${res.status}: ${await res.text()}`);
|
|
158
|
+
if (!res.ok) throw new Error(`jira ${res.status}: ${await res.text()}${createHint(res.status)}`);
|
|
119
159
|
const j = await res.json();
|
|
120
160
|
return { id: j.key, url: `${trim(base)}/browse/${j.key}` };
|
|
121
161
|
},
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# ADR-0014 — Copilot lee SKILL.md nativo (Agent Skills)
|
|
2
|
+
|
|
3
|
+
- **Estado:** aceptado
|
|
4
|
+
- **Fecha:** 2026-07-15
|
|
5
|
+
- **Decide:** lead / arquitecto de la metodología
|
|
6
|
+
- **Modifica:** [ADR-0002](0002-agnostico-del-asistente.md) (la capa 3 del adaptador de Copilot)
|
|
7
|
+
|
|
8
|
+
## Contexto
|
|
9
|
+
|
|
10
|
+
La ADR-0002 definió tres capas: contenido portable (los `SKILL.md`), un CLI determinista
|
|
11
|
+
y **adaptadores generados** por asistente. Para Copilot ese adaptador era
|
|
12
|
+
`skillToPrompt()` → `.github/prompts/<nombre>.prompt.md`, porque los prompt files eran
|
|
13
|
+
el único mecanismo que Copilot tenía. De ahí salía su consecuencia registrada: *"el
|
|
14
|
+
analista sin IDE cierra con Claude Desktop"*, ya que los prompt files son solo-IDE.
|
|
15
|
+
|
|
16
|
+
**Eso dejó de ser cierto.** En diciembre de 2025 GitHub adoptó **Agent Skills**, el
|
|
17
|
+
estándar abierto de `SKILL.md` — el mismo formato que dai ya usa como fuente. Hoy la
|
|
18
|
+
[documentación oficial](https://docs.github.com/en/copilot/concepts/agents/about-agent-skills)
|
|
19
|
+
dice, literal:
|
|
20
|
+
|
|
21
|
+
> "Agent skills work with Copilot cloud agent, Copilot code review, the GitHub Copilot
|
|
22
|
+
> CLI, the GitHub Copilot app, and agent mode in Visual Studio Code and JetBrains IDEs."
|
|
23
|
+
|
|
24
|
+
Con rutas estándar: **`.github/skills`**, `.claude/skills` o `.agents/skills` por repo;
|
|
25
|
+
**`~/.copilot/skills`** o `~/.agents/skills` personales. El frontmatter requerido es
|
|
26
|
+
`name` + `description` — exactamente lo que `validateSkill()` ya exige.
|
|
27
|
+
|
|
28
|
+
Lo descubrimos con un analista funcional real que no lograba que Copilot viera sus
|
|
29
|
+
skills. Tres fallas encadenadas, todas nuestras:
|
|
30
|
+
|
|
31
|
+
1. **`dai skills install` las dejaba en `~/.claude/skills`.** Esa ruta es válida como
|
|
32
|
+
skill personal *en VS Code*, pero **no** en el Copilot CLI ni en la app, que solo
|
|
33
|
+
miran `~/.copilot/skills`. El CLI encima warneaba *"Copilot no tiene skills
|
|
34
|
+
instalables"* — falso desde diciembre.
|
|
35
|
+
2. **`skillToPrompt()` perdía los `templates/`.** Solo leía el `SKILL.md`, así que
|
|
36
|
+
`grill-user-story/templates/user-story.md` nunca viajaba y el prompt quedaba con un
|
|
37
|
+
link roto. Copilot, en cambio, *"automatically discovers all of the files in the
|
|
38
|
+
skill's directory"*.
|
|
39
|
+
3. **La conversión borraba el `name:`** (había un test afirmándolo como correcto), que
|
|
40
|
+
en Agent Skills es **requerido** y es lo que define el `/comando`.
|
|
41
|
+
|
|
42
|
+
## Decisión
|
|
43
|
+
|
|
44
|
+
**El adaptador de Copilot se elimina: dai le entrega el `SKILL.md` tal cual.**
|
|
45
|
+
|
|
46
|
+
- **`dai init --for copilot`** → `.github/skills/<nombre>/` como **copia cruda** (igual
|
|
47
|
+
que Claude), con sus `templates/` adentro. Ya no genera `.github/prompts/`.
|
|
48
|
+
- **`dai skills install --for copilot`** → `~/.copilot/skills/` (global) o
|
|
49
|
+
`.github/skills/` (local). Copilot **sí** tiene skills instalables.
|
|
50
|
+
- **`skillToPrompt()` se borra.** Queda `stalePromptFiles()`: `dai init` **elimina los
|
|
51
|
+
`.prompt.md` que generó dai** en versiones previas — si no, cada `/comando` aparecería
|
|
52
|
+
duplicado, con una copia vieja y sin templates. Solo borra los suyos; los prompt files
|
|
53
|
+
propios del equipo no se tocan.
|
|
54
|
+
- **`dai doctor` chequea Copilot** y, sobre todo, **solo reporta los asistentes que el
|
|
55
|
+
repo realmente usa**. Antes listaba los tres siempre: quien configuraba uno veía 14
|
|
56
|
+
warnings de los otros dos y leía "está todo roto" cuando estaba todo bien.
|
|
57
|
+
- **Cada asistente conserva SU directorio** (`.claude/skills`, `.github/skills`,
|
|
58
|
+
`.cursor/skills`), aunque Copilot sepa leer los tres. La simetría es predecible y
|
|
59
|
+
mantiene la capa 3 de la ADR-0002 donde sigue haciendo falta (Cursor).
|
|
60
|
+
|
|
61
|
+
## Consecuencias
|
|
62
|
+
|
|
63
|
+
- **La conclusión de la ADR-0002 sobre el analista sin IDE queda revocada.** Un
|
|
64
|
+
funcional puede cerrar el ciclo con **Copilot CLI o la app de Copilot**, sin VS Code.
|
|
65
|
+
La tabla del README se corrige: la única superficie sin skills es el chat de
|
|
66
|
+
github.com.
|
|
67
|
+
- **Los `templates/` de las skills llegan a Copilot por primera vez.**
|
|
68
|
+
- **El frontmatter pasó a tener un lector estricto, y eso destapó un defecto viejo.**
|
|
69
|
+
Nuestro `parseFrontmatter` es un regex, no YAML: se tragaba cualquier cosa. Y la
|
|
70
|
+
conversión que borramos hacía `JSON.stringify(description)`, que citaba el valor —
|
|
71
|
+
o sea que **estaba tapando el bug sin querer**. Al entregar el `SKILL.md` crudo, lo
|
|
72
|
+
lee un parser YAML real, y `doc-to-backlog` y `grill-epic` **fallaron al cargar**: sus
|
|
73
|
+
descripciones tenían un `: ` suelto (*"…épicas finales: extrae…"*, *"…nivel
|
|
74
|
+
funcional/alcance: define…"*) que YAML interpreta como el arranque de un mapa. Las dos
|
|
75
|
+
skills que más necesita un analista funcional, invisibles.
|
|
76
|
+
Se citan las descripciones de las 7, y **`validateSkill` ahora valida que `name` y
|
|
77
|
+
`description` sean escalares YAML válidos** — el molde de `templates/skill.md` también
|
|
78
|
+
cita por defecto. La lección general: **un adaptador que "arregla" el input esconde el
|
|
79
|
+
defecto en la fuente**. Mientras convertíamos, el `SKILL.md` inválido era nuestro
|
|
80
|
+
secreto; entregándolo crudo, el contrato es real y hay que cumplirlo.
|
|
81
|
+
- **Menos código, no más:** se borra una transformación y su test. La capa 3 de la
|
|
82
|
+
ADR-0002 sigue siendo la decisión correcta — lo que cambió es que Copilot dejó de
|
|
83
|
+
necesitarla, no que la idea estuviera mal. Cuando el formato de una skill es un
|
|
84
|
+
estándar abierto, el mejor adaptador es ninguno.
|
|
85
|
+
- **Riesgo asumido:** un repo con `--for all` tendrá el mismo skill en `.claude/skills`
|
|
86
|
+
y `.github/skills`, y Copilot mira ambos. El estándar exige `name` único, así que
|
|
87
|
+
esperamos deduplicación por nombre; si en la práctica duplica, se resuelve entonces.
|
|
88
|
+
- **Migración:** `dai init` es idempotente y limpia solo. Un repo viejo se pone al día
|
|
89
|
+
con `dai init --for copilot` (o `dai sync`).
|
|
90
|
+
|
|
91
|
+
## Alternativas descartadas
|
|
92
|
+
|
|
93
|
+
- **Mantener los `.prompt.md` "por compatibilidad".** Duplicaría cada `/comando` con una
|
|
94
|
+
copia peor. Copilot soporta skills en todas sus superficies desde enero; sostener el
|
|
95
|
+
camino viejo es sostener el bug.
|
|
96
|
+
- **Escribir solo `.claude/skills` y que Copilot lo levante** (lo hace: *"if you've
|
|
97
|
+
already set up skills for Claude Code in the `.claude/skills` directory, Copilot will
|
|
98
|
+
pick them up automatically"*). Rompe la simetría del adaptador y sorprende: pedir
|
|
99
|
+
`--for copilot` y no tener nada de Copilot en el repo es un mal contrato.
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# ADR-0015 — `dai publish` en un Jira corporativo (campos propios, épicas, TLS)
|
|
2
|
+
|
|
3
|
+
- **Estado:** aceptado
|
|
4
|
+
- **Fecha:** 2026-07-15
|
|
5
|
+
- **Decide:** lead / arquitecto de la metodología
|
|
6
|
+
|
|
7
|
+
## Contexto
|
|
8
|
+
|
|
9
|
+
`dai publish` funcionaba contra un Jira limpio y fallaba contra uno real. Lo vimos en
|
|
10
|
+
vivo: un analista funcional intentó publicar su primera US en el Jira de su empresa y
|
|
11
|
+
`createUS` mandaba solo `project`, `issuetype`, `summary` y `description`. El proyecto
|
|
12
|
+
exigía un campo propio (`Tipo de trabajo`, `customfield_10042`) → **400 en todos
|
|
13
|
+
los intentos**. Además el proxy corporativo intercepta TLS con su propia CA, y Node no
|
|
14
|
+
usa el trust store del sistema → `fetch` reventaba antes de llegar.
|
|
15
|
+
|
|
16
|
+
**Lo que pasó después es el verdadero motivo de esta ADR.** El asistente, bloqueado por
|
|
17
|
+
el CLI, improvisó: leyó el schema del campo por su cuenta, armó la llamada a la API de
|
|
18
|
+
Jira con `node -e`, y para pasar el proxy corrió `NODE_TLS_REJECT_UNAUTHORIZED=0` —
|
|
19
|
+
apagando la verificación de certificado **mientras mandaba el token de Jira**. Lo
|
|
20
|
+
reportó como *"nota técnica resuelta en el camino"*. Su propio razonamiento fue:
|
|
21
|
+
*"bypassing dai's limitation entirely"*.
|
|
22
|
+
|
|
23
|
+
Publicó bien, de casualidad. Pero eso es exactamente lo que la capa 2 de la
|
|
24
|
+
[ADR-0002](0002-agnostico-del-asistente.md) existe para evitar: *"el output es idéntico
|
|
25
|
+
con Claude, con Copilot, o sin ningún asistente"*. **Cuando el CLI no llega, el agente
|
|
26
|
+
inventa** — y el improviso siguiente puede no respetar el contrato del `ac_hash`, y ahí
|
|
27
|
+
la trazabilidad se rompe en silencio.
|
|
28
|
+
|
|
29
|
+
La lección: **un CLI que no cubre el caso real no es neutral, es una invitación a
|
|
30
|
+
puentearlo.** Cada hueco de la capa 2 se llena con inteligencia no determinista.
|
|
31
|
+
|
|
32
|
+
## Decisión
|
|
33
|
+
|
|
34
|
+
**1. Los campos propios se declaran, con nombre humano y opciones válidas.**
|
|
35
|
+
`.dai/jira-fields.json` (o `DAI_JIRA_FIELDS_FILE`), por issuetype:
|
|
36
|
+
|
|
37
|
+
```json
|
|
38
|
+
{
|
|
39
|
+
"Story": {
|
|
40
|
+
"clasificacion": {
|
|
41
|
+
"field": "customfield_10042",
|
|
42
|
+
"shape": "select",
|
|
43
|
+
"default": "Mejora",
|
|
44
|
+
"options": ["Mejora", "Corrección"]
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
- **Por issuetype**, porque en Jira corporativo los campos obligatorios de una Epic y de
|
|
51
|
+
una Story casi nunca son los mismos.
|
|
52
|
+
- **`default` + `--field alias=valor`**, porque el valor **no es config fija**: la
|
|
53
|
+
clasificación es Mejora o Corrección *según la US*. Un default fijo publicaría todas
|
|
54
|
+
iguales — un dato incorrecto en Jira, en silencio, que es peor que fallar.
|
|
55
|
+
- **`options` valida ANTES de la red.** Un typo da `'Mejraa' no es opción de
|
|
56
|
+
'clasificacion' — válidas: Mejora | Corrección`, no un 400 críptico.
|
|
57
|
+
- **Un campo declarado tiene que resolver** (default u override): se declara porque Jira
|
|
58
|
+
lo exige. Si es opcional, no se declara.
|
|
59
|
+
- **`shape`** cubre las formas de Jira (`select` `{value:X}` · `text` X · `multi`
|
|
60
|
+
`[{value:X}]`), con **`raw`** como escape hatch para cualquier cosa rara: dai no
|
|
61
|
+
necesita entender todo Jira para no estorbar.
|
|
62
|
+
- **Sin archivo → `{}`.** Un Jira sin campos obligatorios publica igual que siempre.
|
|
63
|
+
|
|
64
|
+
**2. `--parent` y `--issuetype`.** `--parent` cuelga la US de su épica (`createUS` nunca
|
|
65
|
+
mandaba `parent`); `--issuetype Epic` permite que **`grill-epic` publique épicas por
|
|
66
|
+
CLI**, que hasta hoy no tenían fallback y quedaban como un `.md` para pegar a mano.
|
|
67
|
+
|
|
68
|
+
**3. Un fallo de TLS enseña el camino correcto, y desaconseja el peligroso.** `daiFetch`
|
|
69
|
+
traduce los códigos de certificado a un error que manda a **`NODE_EXTRA_CA_CERTS`** y
|
|
70
|
+
dice explícitamente por qué `NODE_TLS_REJECT_UNAUTHORIZED=0` no es una alternativa. dai
|
|
71
|
+
**nunca** baja la verificación por su cuenta.
|
|
72
|
+
|
|
73
|
+
**4. `DAI_JIRA_PROJECT` se valida.** `PROJ-42` es un ticket, no un proyecto — el
|
|
74
|
+
error de config más común. El mensaje da los dos caminos: `DAI_JIRA_PROJECT=PROJ`, o
|
|
75
|
+
`--parent PROJ-42` si lo que querías era colgarla de esa épica.
|
|
76
|
+
|
|
77
|
+
**5. La constitución prohíbe las dos maniobras**, para todos los asistentes:
|
|
78
|
+
|
|
79
|
+
> - **No bajes la seguridad para avanzar:** si una llamada falla por el certificado,
|
|
80
|
+
> declara la CA. Nunca `NODE_TLS_REJECT_UNAUTHORIZED=0`, `verify=False`, `-k`.
|
|
81
|
+
> - **Si el CLI no llega, para y dilo:** no improvises una llamada a la API por fuera.
|
|
82
|
+
|
|
83
|
+
## Consecuencias
|
|
84
|
+
|
|
85
|
+
- **`dai publish` sirve en un Jira corporativo**, que es donde vive el usuario real.
|
|
86
|
+
- **`grill-epic` gana un fallback CLI**: MCP → `dai publish --issuetype Epic` → `.md`.
|
|
87
|
+
- **El caso que motivó el bypass ahora está cubierto**, que es la única forma honesta de
|
|
88
|
+
pedirle a un agente que no improvise: no dejarle el hueco.
|
|
89
|
+
- **`dai doctor`** valida la clave de proyecto y que el archivo de campos parsee. Sigue
|
|
90
|
+
**sin** verificar que el token sirva — eso es red, y lo dice `dai publish`. El chequeo
|
|
91
|
+
ahora lo admite en voz alta en vez de dar un ✓ engañoso.
|
|
92
|
+
- **ClickUp queda con el mismo hueco de TLS.** `daiFetch` está listo para adoptarse allá
|
|
93
|
+
con un import; no lo hicimos ahora por alcance.
|
|
94
|
+
|
|
95
|
+
## Alternativas descartadas
|
|
96
|
+
|
|
97
|
+
- **Una sola variable `DAI_JIRA_FIELDS={"customfield_10042":{"value":"Mejora"}}`.**
|
|
98
|
+
Una línea ilegible, sin distinguir Story de Epic, sin validación, y con el valor fijo
|
|
99
|
+
— el problema original.
|
|
100
|
+
- **Resolver los nombres de campo contra `createmeta` de Jira.** Menos config, pero pide
|
|
101
|
+
red para armar el payload, y falla distinto según permisos. Un archivo explícito se lee
|
|
102
|
+
y se versiona.
|
|
103
|
+
- **Que dai reintente sin verificar TLS si el certificado falla.** Es exactamente lo que
|
|
104
|
+
hizo el agente. Automatizarlo sería institucionalizar el bug.
|
package/docs/adr/README.md
CHANGED
|
@@ -17,6 +17,10 @@ decisión cambia, se escribe un ADR nuevo que supersede al viejo. Molde en
|
|
|
17
17
|
| [0009](0009-adaptador-cursor.md) | Adaptador nativo para Cursor (skills + rules) con `dai init`/`install`/`doctor` | propuesto |
|
|
18
18
|
| [0010](0010-versionado-y-upgrade.md) | Versionado y upgrade: compatibilidad por semver, `doctor` version-drift, `dai sync` aditivo | propuesto |
|
|
19
19
|
| [0011](0011-archive-gate-de-aprobacion.md) | `archive` es un gate de aprobación: `dai archive` (comando) lo corre el aprobador; `check`/`ls` saltean `archive/` | aceptado |
|
|
20
|
+
| [0012](0012-upgrade-self-update-del-cli.md) | `dai upgrade`: self-update del CLI + `dai sync` del scaffold | aceptado |
|
|
21
|
+
| [0013](0013-skills-externas-install-from.md) | `dai skills install --from`: skills externas por-stack, sin registro ni gatekeeping | aceptado |
|
|
22
|
+
| [0014](0014-copilot-agent-skills.md) | Copilot lee `SKILL.md` nativo (Agent Skills): se elimina el adaptador `.prompt.md` — modifica la 0002 | aceptado |
|
|
23
|
+
| [0015](0015-jira-corporativo.md) | `dai publish` en Jira corporativo: campos propios declarados, `--parent`/`--issuetype`, TLS con CA (nunca apagar la verificación) | aceptado |
|
|
20
24
|
|
|
21
25
|
> Estas son las decisiones que cierran las "Decisiones abiertas" de
|
|
22
26
|
> [`METODOLOGIA.md §7`](../METODOLOGIA.md) y las enmiendas al
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dforce2055/dai",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.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/",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: dai-review
|
|
3
|
-
description: Revisa una Pull/Merge Request de un repo remoto (GitHub o GitLab) de forma consciente de la metodología dai, y deja un comentario estándar en español con errores y mejoras. Corre `dai check` (¿la US está atrasada?), valida el Definition of Done, hace el review de código (correctitud + calidad), compone el comentario estándar y lo postea — vía el MCP del forge si está disponible, o vía `dai forge comment` (token) si no. Invocar como /dai-review <URL-de-la-PR o número>. Usar en el paso 6 de SCRUM-CON-IA (code review), antes de que un partner humano firme.
|
|
3
|
+
description: "Revisa una Pull/Merge Request de un repo remoto (GitHub o GitLab) de forma consciente de la metodología dai, y deja un comentario estándar en español con errores y mejoras. Corre `dai check` (¿la US está atrasada?), valida el Definition of Done, hace el review de código (correctitud + calidad), compone el comentario estándar y lo postea — vía el MCP del forge si está disponible, o vía `dai forge comment` (token) si no. Invocar como /dai-review <URL-de-la-PR o número>. Usar en el paso 6 de SCRUM-CON-IA (code review), antes de que un partner humano firme."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# dai-review — review de PR consciente de la metodología
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: doc-to-backlog
|
|
3
|
-
description: Toma un documento de análisis (PDF, Word, o un archivo en Drive/SharePoint/ClickUp vía MCP) y produce un BACKLOG CANDIDATO — un mapa de épicas y User Stories propuestas — para que el funcional lo priorice y lo valide. NO emite US ni épicas finales: extrae candidatos marcados "sin validar" y hace handoff de cada sobreviviente a grill-epic / grill-user-story (modo refinar). Es la puerta de entrada del caso "llegué con un documento y quiero sacar el backlog". Invocar como /doc-to-backlog con el path o el link del documento. Usar antes de grill-epic / grill-user-story cuando el input es un doc grande.
|
|
3
|
+
description: "Toma un documento de análisis (PDF, Word, o un archivo en Drive/SharePoint/ClickUp vía MCP) y produce un BACKLOG CANDIDATO — un mapa de épicas y User Stories propuestas — para que el funcional lo priorice y lo valide. NO emite US ni épicas finales: extrae candidatos marcados \"sin validar\" y hace handoff de cada sobreviviente a grill-epic / grill-user-story (modo refinar). Es la puerta de entrada del caso \"llegué con un documento y quiero sacar el backlog\". Invocar como /doc-to-backlog con el path o el link del documento. Usar antes de grill-epic / grill-user-story cuando el input es un doc grande."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# doc-to-backlog
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: grill-epic
|
|
3
|
-
description: Interroga a un PO o analista funcional para producir una ÉPICA bien formada — un bloque grande de valor de negocio que se parte en varias User Stories — siguiendo el template del método. O toma una US que resultó demasiado grande y la promueve a épica. Se queda a nivel funcional/alcance: define el objetivo de negocio y la partición en US, NUNCA criterios de aceptación (esos viven en cada US) ni diseño técnico. Al terminar, publica la épica en Jira/ClickUp (o deja un .md) y hace handoff de cada US hija a grill-user-story. Invocar como /grill-epic, opcionalmente con un título, un ID/URL del tracker, o una US grande a promover. Usar cuando algo es demasiado grande para una sola US.
|
|
3
|
+
description: "Interroga a un PO o analista funcional para producir una ÉPICA bien formada — un bloque grande de valor de negocio que se parte en varias User Stories — siguiendo el template del método. O toma una US que resultó demasiado grande y la promueve a épica. Se queda a nivel funcional/alcance: define el objetivo de negocio y la partición en US, NUNCA criterios de aceptación (esos viven en cada US) ni diseño técnico. Al terminar, publica la épica en Jira/ClickUp (o deja un .md) y hace handoff de cada US hija a grill-user-story. Invocar como /grill-epic, opcionalmente con un título, un ID/URL del tracker, o una US grande a promover. Usar cuando algo es demasiado grande para una sola US."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# grill-epic
|
|
@@ -60,10 +60,18 @@ la forma. Nunca reescribas el formato inline.
|
|
|
60
60
|
1. **Armar la épica** con el formato de `templates/epica.md`: metadata (`ID`, autor,
|
|
61
61
|
estado, US que la componen) + objetivo + alcance (in/out) + lista de US + métricas +
|
|
62
62
|
dependencias.
|
|
63
|
-
2. **Publicar en el tracker**
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
63
|
+
2. **Publicar en el tracker.** **No asumas el tracker: lee `DAI_PM` del `.env` primero.**
|
|
64
|
+
Tres caminos, mismo contenido:
|
|
65
|
+
- **Con MCP** (`jira` → MCP de Atlassian · `clickup` → MCP de ClickUp): crea el ticket
|
|
66
|
+
de épica; las US hijas se crean como tickets vinculados (o quedan listadas).
|
|
67
|
+
- **Sin MCP, con token** (`DAI_PM=jira` + token en `.env`): escribe la épica como `.md`
|
|
68
|
+
y publícala con **`dai publish <epica.md> --issuetype Epic`**. Devuelve el key. Si el
|
|
69
|
+
proyecto exige campos propios, van con `--field alias=valor` (los declarados en
|
|
70
|
+
`.dai/jira-fields.json`; `dai doctor` te los lista). Después, cada US hija se cuelga
|
|
71
|
+
con `dai publish <us.md> --parent <KEY-de-la-épica>`.
|
|
72
|
+
- **Sin token** (o `DAI_PM=md`): deja el `.md` para pegar a mano, y avisa el motivo.
|
|
73
|
+
- Si `dai` falla, **para y reporta el error tal cual**. No improvises una llamada a la
|
|
74
|
+
API del tracker por fuera: publicaría igual pero rompería el link en silencio.
|
|
67
75
|
3. **Handoff.** Ofrecer pasar **cada US hija** por `/grill-user-story` para convertirla
|
|
68
76
|
de "título en la lista" a US testeable con criterios. Ese es el paso que las hace
|
|
69
77
|
linkeables (`dai link-us`).
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: grill-intent
|
|
3
|
-
description: Gate 0 of the SDD workflow — challenge the *problem* behind a clean user story before any spec is generated. Interrogates whether this is the right problem, for the right user, within constraints, and whether the story's implied solution is a premature jump. Produces intent.md and a verdict that can be "go to spec", "reframe", or "don't build". In the spirit of grill-me, specialized for problem-challenge. Invoke as /grill-intent on a user story (the output of grill-user-story). Use after grill-user-story and before openspec propose.
|
|
3
|
+
description: "Gate 0 of the SDD workflow — challenge the *problem* behind a clean user story before any spec is generated. Interrogates whether this is the right problem, for the right user, within constraints, and whether the story's implied solution is a premature jump. Produces intent.md and a verdict that can be \"go to spec\", \"reframe\", or \"don't build\". In the spirit of grill-me, specialized for problem-challenge. Invoke as /grill-intent on a user story (the output of grill-user-story). Use after grill-user-story and before openspec propose."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# grill-intent — Gate 0, challenge the problem
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: grill-user-story
|
|
3
|
-
description: Interroga a un PO o analista funcional para producir una User Story funcional y testeable siguiendo el formato del modelo de trazabilidad — o pule una US vaga existente. Se queda a nivel funcional/usuario y se niega a derivar en diseño técnico o a emitir una US con criterios no testeables. Al terminar, PUBLICA la US en el tracker configurado del repo (Jira o ClickUp, según DAI_PM del .env) usando su MCP; si no hay MCP/token, deja un .md con el mismo formato para copiar y pegar. Invocar como /grill-user-story, opcionalmente con un título, un ID/URL del tracker, y/o una US rústica existente. Usar antes de opsx:propose, o cuando alguien dice "necesito una US", "convierte esto en una US como corresponde", o "esta historia está muy vaga".
|
|
3
|
+
description: "Interroga a un PO o analista funcional para producir una User Story funcional y testeable siguiendo el formato del modelo de trazabilidad — o pule una US vaga existente. Se queda a nivel funcional/usuario y se niega a derivar en diseño técnico o a emitir una US con criterios no testeables. Al terminar, PUBLICA la US en el tracker configurado del repo (Jira o ClickUp, según DAI_PM del .env) usando su MCP; si no hay MCP/token, deja un .md con el mismo formato para copiar y pegar. Invocar como /grill-user-story, opcionalmente con un título, un ID/URL del tracker, y/o una US rústica existente. Usar antes de opsx:propose, o cuando alguien dice \"necesito una US\", \"convierte esto en una US como corresponde\", o \"esta historia está muy vaga\"."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# grill-user-story
|
|
@@ -65,11 +65,24 @@ La US se produce UNA vez con el formato de `../../templates/formato-us.md`. Lo q
|
|
|
65
65
|
- Si NO hay MCP del tracker conectado (pero sí `DAI_PM=jira|clickup` + token en `.env`):
|
|
66
66
|
escribe la US como `.md` (formato `formato-us.md`) y publícala con el comando:
|
|
67
67
|
**`dai publish <ruta-del-md>`** → crea el issue/tarea vía REST y devuelve el key.
|
|
68
|
-
(Jira necesita además `DAI_JIRA_PROJECT` en el `.env
|
|
68
|
+
(Jira necesita además `DAI_JIRA_PROJECT` en el `.env` — la clave del **proyecto**,
|
|
69
|
+
`PROJ`, no la de un ticket.)
|
|
70
|
+
- **Si la US pertenece a una épica:** `dai publish <us.md> --parent <KEY-de-la-épica>`.
|
|
71
|
+
- **Si el proyecto exige campos propios** (típico en Jira corporativo): van con
|
|
72
|
+
`--field alias=valor`, repetible. Los alias son los de `.dai/jira-fields.json`, y
|
|
73
|
+
`dai doctor` te dice cuáles hay. Si el valor cambia según la US (p. ej. una
|
|
74
|
+
clasificación Mejora/Corrección), **pregúntaselo a la persona** durante el
|
|
75
|
+
interrogatorio — no lo elijas tú, es una decisión de negocio.
|
|
76
|
+
Ej.: `dai publish us.md --parent PROJ-42 --field clasificacion=Corrección`
|
|
69
77
|
- Si tampoco hay token (o `DAI_PM=md`): deja solo el `.md` para que la persona lo
|
|
70
78
|
pegue a mano en el tracker. Avisa el motivo.
|
|
71
79
|
- El contenido es **idéntico** en los tres caminos (MCP / `dai publish` / manual).
|
|
72
|
-
4. **
|
|
80
|
+
4. **Si `dai publish` falla, para y reporta el error tal cual.** No improvises una llamada
|
|
81
|
+
a la API del tracker por fuera, ni bajes la verificación TLS para pasar un proxy: el
|
|
82
|
+
atajo publica igual, pero el `## Criterios de aceptación` puede quedar mal formado y
|
|
83
|
+
**el link QUÉ↔CÓMO se rompe en silencio**. Si el error nombra un campo obligatorio que
|
|
84
|
+
falta, decláralo en `.dai/jira-fields.json` (molde en `.dai/templates/`) y reintenta.
|
|
85
|
+
5. **Estado.** Dejar la US en `pulida`. Ofrecer que el dev siga con `dai link-us <ID>` → `opsx:propose` (lado técnico).
|
|
73
86
|
|
|
74
87
|
## Hand-off
|
|
75
88
|
|
package/skills/link-us/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: link-us
|
|
3
|
-
description: Del lado del dev — crea la branch desde el link de la User Story en Jira SIN margen de error y genera el archivo implements.yaml con el link a la US (id + version + ac_hash). Es la que hace que el link QUÉ↔CÓMO sea correcto por construcción, sin que el dev tipee el key a mano. Invocar como /link-us ABC-### (o con la URL del ticket de Jira). Usar al arrancar la implementación de una US, antes o junto con opsx:explore / opsx:propose.
|
|
3
|
+
description: "Del lado del dev — crea la branch desde el link de la User Story en Jira SIN margen de error y genera el archivo implements.yaml con el link a la US (id + version + ac_hash). Es la que hace que el link QUÉ↔CÓMO sea correcto por construcción, sin que el dev tipee el key a mano. Invocar como /link-us ABC-### (o con la URL del ticket de Jira). Usar al arrancar la implementación de una US, antes o junto con opsx:explore / opsx:propose."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# link-us
|