agentic-workflow-manager 3.13.4 → 3.13.5

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.
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.matchFrontmatterBlock = matchFrontmatterBlock;
6
+ exports.findFrontmatterDescription = exports.isBlockScalarHeader = exports.readFrontmatterDescription = exports.matchFrontmatterBlock = void 0;
7
7
  exports.readArtifactDescription = readArtifactDescription;
8
8
  exports.discoverSkills = discoverSkills;
9
9
  exports.discoverWorkflows = discoverWorkflows;
@@ -12,31 +12,23 @@ exports.discoverAgents = discoverAgents;
12
12
  const fs_1 = __importDefault(require("fs"));
13
13
  const path_1 = __importDefault(require("path"));
14
14
  const registries_1 = require("./registries");
15
- /** Extracts the raw frontmatter text (between the --- delimiters), or null if the block is missing/malformed. CRLF-tolerant. */
16
- function matchFrontmatterBlock(raw) {
17
- const fmMatch = raw.match(/^---\r?\n([\s\S]*?)\r?\n---/);
18
- return fmMatch ? fmMatch[1] : null;
19
- }
15
+ // El parseo de frontmatter vive en el modulo HOJA `core/frontmatter.ts`
16
+ // (sin imports, para que consumidores puros como export/transform.ts no
17
+ // arrastren fs/git por transitividad). Se re-exporta aca porque este modulo
18
+ // ya era el punto de entrada historico para esos helpers.
19
+ var frontmatter_1 = require("./frontmatter");
20
+ Object.defineProperty(exports, "matchFrontmatterBlock", { enumerable: true, get: function () { return frontmatter_1.matchFrontmatterBlock; } });
21
+ Object.defineProperty(exports, "readFrontmatterDescription", { enumerable: true, get: function () { return frontmatter_1.readFrontmatterDescription; } });
22
+ Object.defineProperty(exports, "isBlockScalarHeader", { enumerable: true, get: function () { return frontmatter_1.isBlockScalarHeader; } });
23
+ Object.defineProperty(exports, "findFrontmatterDescription", { enumerable: true, get: function () { return frontmatter_1.findFrontmatterDescription; } });
24
+ const frontmatter_2 = require("./frontmatter");
20
25
  function readArtifactDescription(filePath) {
21
26
  try {
22
27
  const raw = fs_1.default.readFileSync(filePath, 'utf-8');
23
- const frontmatter = matchFrontmatterBlock(raw);
28
+ const frontmatter = (0, frontmatter_2.matchFrontmatterBlock)(raw);
24
29
  if (frontmatter === null)
25
30
  return '';
26
- const line = frontmatter
27
- .split(/\r?\n/)
28
- .find((l) => /^description\s*:/.test(l));
29
- if (!line)
30
- return '';
31
- let val = line.replace(/^description\s*:/, '').trim();
32
- if ((val.startsWith('"') && val.endsWith('"')) ||
33
- (val.startsWith("'") && val.endsWith("'"))) {
34
- val = val.slice(1, -1);
35
- }
36
- const BLOCK_INDICATORS = new Set(['>-', '>', '|-', '|', '>+', '|+']);
37
- if (BLOCK_INDICATORS.has(val.trim()))
38
- return '';
39
- return val.trim();
31
+ return (0, frontmatter_2.readFrontmatterDescription)(frontmatter);
40
32
  }
41
33
  catch {
42
34
  return '';
@@ -7,7 +7,10 @@ exports.claudeAiTransform = claudeAiTransform;
7
7
  //
8
8
  // Transform mecánico claude.ai (R3.1): función pura string → string.
9
9
  // Frontmatter line-based plano (los SKILL.md del baseline usan claves de una
10
- // línea) — sin parser YAML a propósito (YAGNI, cero deps).
10
+ // línea) — sin parser YAML a propósito (YAGNI, cero deps). La única forma
11
+ // multilínea soportada es el block scalar de `description:`, resuelto vía la
12
+ // función compartida de discovery.ts (ver claudeAiTransform).
13
+ const frontmatter_1 = require("../frontmatter");
11
14
  const DEFERENCE_LINE = (skillName) => `In environments with AWM installed (Claude Code), defer to the registry's ${skillName} skill — this port is for environments without filesystem access.`;
12
15
  exports.DEFERENCE_LINE = DEFERENCE_LINE;
13
16
  // Paths intra-registry: resuelven en Claude Code (donde el registry está en
@@ -73,34 +76,36 @@ function claudeAiTransform(skillMd, skillName) {
73
76
  const body = skillMd.slice(end + endMatch[0].length);
74
77
  const fmLines = skillMd.slice(startLen, end).split(/\r?\n/)
75
78
  .filter((l) => !/^(version|portable):/.test(l));
76
- const descIdx = fmLines.findIndex((l) => /^description:/.test(l));
77
- if (descIdx === -1) {
79
+ // UN SOLO camino para todas las formas de `description`. Antes habia uno por
80
+ // forma (plano / entrecomillado / bloque) y cada vez que el LECTOR aprendia
81
+ // una forma nueva, el ESCRITOR de aca quedaba atras — la divergencia exacta
82
+ // que este modulo dice cerrar. Casos reales que produjo esa asimetria:
83
+ // - escalar plano multilinea: la deference line se insertaba en la primera
84
+ // linea y la continuacion quedaba huerfana debajo, o sea la frase de
85
+ // deference terminaba enterrada en el medio de la descripcion;
86
+ // - escalar plano con ` # comentario` final: la deference line se anexaba
87
+ // DESPUES del `#`, o sea YAML se la comia entera como comentario y el
88
+ // artefacto exportado perdia en silencio la unica frase que este
89
+ // transform existe para agregar.
90
+ // Ahora se resuelve el campo con la funcion compartida, se reemplaza su
91
+ // extension COMPLETA (startLine..endLine) y se emite siempre un escalar
92
+ // double-quoted via JSON.stringify — superset valido de YAML que cubre
93
+ // comillas, `:`, `#` y los `\n` de un literal `|` sin decidir estilo.
94
+ const field = (0, frontmatter_1.findFrontmatterDescription)(fmLines.join('\n'));
95
+ if (field.startLine === -1) {
78
96
  throw new Error('frontmatter has no description field');
79
97
  }
80
- const descLine = fmLines[descIdx];
81
- const value = descLine.slice('description:'.length).trim();
82
- if (value === '' || value === '>' || value === '|' || value.startsWith('>') || value.startsWith('|')) {
83
- throw new Error('description must be single-line (block scalars are not supported by the export transform)');
98
+ const rawValue = fmLines[field.startLine].replace(/^description\s*:/, '').trim();
99
+ // Empieza con `>`/`|` pero no es un indicador bien formado (ej. `>-basura`):
100
+ // YAML mismo lo rechaza. Fallar explicito en vez de publicar basura.
101
+ if (/^[>|]/.test(rawValue) && !(0, frontmatter_1.isBlockScalarHeader)(rawValue)) {
102
+ throw new Error(`malformed block scalar indicator in description: ${rawValue}`);
84
103
  }
85
- const deference = (0, exports.DEFERENCE_LINE)(skillName);
86
- // Quote-style detection mirrors readArtifactDescription in discovery.ts: both
87
- // single- and double-quoted scalars are first-class, and we work off the
88
- // trimmed value so trailing whitespace after a closing quote doesn't fool us.
89
- const isDoubleQuoted = value.length >= 2 && value.startsWith('"') && value.endsWith('"');
90
- const isSingleQuoted = value.length >= 2 && value.startsWith("'") && value.endsWith("'");
91
- if ((value.startsWith('"') || value.startsWith("'")) && !isDoubleQuoted && !isSingleQuoted) {
92
- throw new Error('description has trailing content after its closing quote (e.g. an inline comment) — not supported by the export transform; remove the comment or use a port.claude-ai.md override');
104
+ if (!field.value) {
105
+ throw new Error((0, frontmatter_1.isBlockScalarHeader)(rawValue) ? 'description block scalar has no content' : 'description is empty');
93
106
  }
94
- // YAML single-quoted scalars escape a literal ' by doubling it (''); the
95
- // deference text ("...registry's..." see DEFERENCE_LINE) contains an
96
- // apostrophe, so it must be escaped before splicing into a single-quoted
97
- // description or it would prematurely close the YAML string.
98
- const newValue = isDoubleQuoted
99
- ? `${value.slice(0, -1)} ${deference}"`
100
- : isSingleQuoted
101
- ? `${value.slice(0, -1)} ${deference.replace(/'/g, "''")}'`
102
- : `${value} ${deference}`;
103
- fmLines[descIdx] = `description: ${newValue}`;
107
+ const merged = JSON.stringify(`${field.value} ${(0, exports.DEFERENCE_LINE)(skillName)}`);
108
+ fmLines.splice(field.startLine, field.endLine - field.startLine + 1, `description: ${merged}`);
104
109
  // Solo el body: el frontmatter ya se editó arriba y sus campos no son prosa
105
110
  // navegable (R2.4).
106
111
  return `---\n${fmLines.join('\n')}\n---\n${stripIntraRegistryPaths(body)}`;
@@ -0,0 +1,223 @@
1
+ "use strict";
2
+ // src/core/frontmatter.ts
3
+ //
4
+ // Modulo HOJA: sin imports. Es la unica fuente de verdad para leer el
5
+ // frontmatter YAML de un artefacto (SKILL.md, agents/*.md, workflows/*.md).
6
+ //
7
+ // Por que hoja: sus consumidores incluyen `core/export/transform.ts`, que se
8
+ // documenta como funcion pura string -> string sin dependencias. Si esto
9
+ // viviera en `discovery.ts` (que importa `registries.ts`, y este a su vez
10
+ // resuelve `awmHome()` y carga `simple-git` en tiempo de import), un modulo
11
+ // puro terminaria arrastrando la capa de fs/git por transitividad.
12
+ //
13
+ // Por que existe: el frontmatter se parseaba a mano en CUATRO lugares, cada
14
+ // uno con su propia copia incompleta. Un block scalar YAML valido
15
+ // (`description: >-`, con el texto en las lineas indentadas siguientes)
16
+ // degradaba en silencio en discovery, crasheaba `awm add -a cursor|copilot`,
17
+ // abortaba `awm export <bundle>` entero y rompia `awm add -a codex`. La
18
+ // semantica de aca se verifica contra js-yaml (parser real) en
19
+ // tests/core/frontmatter-description-vs-yaml.test.ts — no contra valores
20
+ // esperados escritos a mano, que pueden envejecer hacia la expectativa
21
+ // equivocada.
22
+ Object.defineProperty(exports, "__esModule", { value: true });
23
+ exports.matchFrontmatterBlock = matchFrontmatterBlock;
24
+ exports.isBlockScalarHeader = isBlockScalarHeader;
25
+ exports.findFrontmatterDescription = findFrontmatterDescription;
26
+ exports.readFrontmatterDescription = readFrontmatterDescription;
27
+ /** Texto crudo del frontmatter (entre los delimitadores `---`), o null si el
28
+ * bloque falta o esta mal formado. Tolera CRLF. */
29
+ function matchFrontmatterBlock(raw) {
30
+ const fmMatch = raw.match(/^---\r?\n([\s\S]*?)\r?\n---/);
31
+ return fmMatch ? fmMatch[1] : null;
32
+ }
33
+ /** Indicador de block scalar en la linea de la clave: estilo (`>` folded /
34
+ * `|` literal) + chomping opcional (`-`/`+`) + indentacion explicita opcional
35
+ * + comentario final opcional. Detalles verificados contra js-yaml:
36
+ * - la indentacion explicita es UN digito 1-9 (ni `0` ni `10`);
37
+ * - `>- # nota` es valido y su bloque se lee normal;
38
+ * - `>-basura` NO es valido y aca tampoco matchea. */
39
+ const BLOCK_SCALAR_HEADER = /^([>|])(?:([+-])?([1-9])?|([1-9])?([+-])?)(?:\s+#.*)?$/;
40
+ /** ¿El valor de la linea de la clave es un indicador de block scalar?
41
+ * Cualquier caller que RAMIFIQUE sobre "esto es un bloque" debe usar esta
42
+ * funcion, nunca una aproximacion propia: una guarda por prefijo (`/^[>|]/`)
43
+ * y un resolver por match completo se desincronizan, y en ese hueco el
44
+ * escritor borro contenido real creyendo que resolvia un bloque. */
45
+ function isBlockScalarHeader(value) {
46
+ return BLOCK_SCALAR_HEADER.test(value.trim());
47
+ }
48
+ /** Indice de la comilla que CIERRA el escalar que empieza en la posicion 0, o
49
+ * -1 si no cierra. Cada estilo escapa distinto: en single-quoted un `''` es
50
+ * un apostrofe literal (no un cierre); en double-quoted un `\"` es una
51
+ * comilla literal. */
52
+ function findClosingQuote(value, quote) {
53
+ for (let i = 1; i < value.length; i++) {
54
+ if (quote === '"' && value[i] === '\\') {
55
+ i++;
56
+ continue;
57
+ }
58
+ if (value[i] !== quote)
59
+ continue;
60
+ if (quote === "'" && value[i + 1] === "'") {
61
+ i++;
62
+ continue;
63
+ }
64
+ return i;
65
+ }
66
+ return -1;
67
+ }
68
+ function foldBlock(stripped, isLiteral) {
69
+ if (isLiteral)
70
+ return stripped.join('\n').trim();
71
+ // Folded (`>`): las lineas al nivel base del bloque se pliegan entre si con
72
+ // UN espacio; una linea MAS indentada no se pliega (conserva su salto y su
73
+ // sangria — la regla que permite incrustar una lista dentro de un folded);
74
+ // y k lineas en blanco consecutivas producen k saltos de linea.
75
+ // Ojo: NO se hace trim por linea — YAML conserva los espacios finales de
76
+ // cada linea y el plegado agrega el suyo encima.
77
+ let out = '';
78
+ let prevLiteral = false;
79
+ let started = false;
80
+ let blanks = 0;
81
+ for (const line of stripped) {
82
+ if (line === '') {
83
+ blanks++;
84
+ continue;
85
+ }
86
+ const literal = /^[ \t]/.test(line);
87
+ if (!started) {
88
+ out = line;
89
+ started = true;
90
+ prevLiteral = literal;
91
+ blanks = 0;
92
+ continue;
93
+ }
94
+ if (blanks > 0) {
95
+ out += '\n'.repeat(blanks);
96
+ blanks = 0;
97
+ }
98
+ else if (literal || prevLiteral)
99
+ out += '\n';
100
+ else
101
+ out += ' ';
102
+ out += line;
103
+ prevLiteral = literal;
104
+ }
105
+ return out.trim();
106
+ }
107
+ /**
108
+ * Localiza y RESUELVE el campo `description` de un bloque de frontmatter,
109
+ * devolviendo tambien la extension exacta que ocupa (para que un escritor
110
+ * pueda reemplazarlo entero).
111
+ *
112
+ * Formas soportadas, todas contrastadas contra js-yaml: escalar plano (de una
113
+ * o varias lineas), escalar entrecomillado (simple y doble, con su escape
114
+ * propio deshecho), y block scalar folded/literal con chomping e indentacion
115
+ * explicita.
116
+ */
117
+ function findFrontmatterDescription(frontmatter) {
118
+ const lines = frontmatter.split(/\r?\n/);
119
+ // Ancla en COLUMNA 0 a proposito: `description` en un frontmatter es
120
+ // siempre clave de nivel superior. Un `^\s*` haria ganar a un
121
+ // `description:` INDENTADO — anidado en otro mapa, o una linea de
122
+ // CONTENIDO de otro block scalar que casualmente empiece asi.
123
+ const startLine = lines.findIndex((l) => /^description\s*:/.test(l));
124
+ if (startLine === -1)
125
+ return { value: '', startLine: -1, endLine: -1 };
126
+ const raw = lines[startLine].replace(/^description\s*:/, '').trim();
127
+ if (isBlockScalarHeader(raw)) {
128
+ const isLiteral = raw.trim().startsWith('|');
129
+ // El bloque son las lineas ESTRICTAMENTE indentadas respecto de la
130
+ // clave (columna 0). Las lineas en blanco no lo cortan: pertenecen a el.
131
+ const block = [];
132
+ let last = startLine;
133
+ for (let i = startLine + 1; i < lines.length; i++) {
134
+ if (lines[i].trim() === '') {
135
+ block.push(lines[i]);
136
+ continue;
137
+ }
138
+ if (!/^[ \t]/.test(lines[i]))
139
+ break;
140
+ block.push(lines[i]);
141
+ last = i;
142
+ }
143
+ while (block.length > 0 && block[block.length - 1].trim() === '')
144
+ block.pop();
145
+ const first = block.find((l) => l.trim() !== '');
146
+ if (first === undefined)
147
+ return { value: '', startLine, endLine: startLine };
148
+ const declared = raw.match(/[1-9]/);
149
+ const blockIndent = declared !== null ? Number(declared[0]) : first.match(/^[ \t]*/)[0].length;
150
+ // Una linea con MENOS indentacion que el bloque es YAML invalido
151
+ // (js-yaml lanza "bad indentation"). Cortar a ciegas con slice()
152
+ // comeria caracteres reales: preferimos no inventar un valor.
153
+ if (block.some((l) => l.trim() !== '' && l.match(/^[ \t]*/)[0].length < blockIndent)) {
154
+ return { value: '', startLine, endLine: last };
155
+ }
156
+ const stripped = block.map((l) => l.slice(blockIndent));
157
+ return { value: foldBlock(stripped, isLiteral), startLine, endLine: last };
158
+ }
159
+ // Empieza con `>`/`|` pero NO es un indicador bien formado (`>0`, `>12`,
160
+ // `>-basura`): YAML lo rechaza. Devolver '' hace que los consumidores
161
+ // fallen fuerte (throw en parseSkillSource / claudeAiTransform) en vez de
162
+ // caer al camino de escalar plano de abajo, que publicaria el indicador
163
+ // mismo como si fuera la descripcion.
164
+ if (/^[>|]/.test(raw))
165
+ return { value: '', startLine, endLine: startLine };
166
+ // Continuacion multilinea. Aplica a DOS formas, y ambas la necesitan:
167
+ // - escalar plano (`description: primera` + lineas indentadas), que YAML
168
+ // pliega con espacios igual que un `>`;
169
+ // - escalar ENTRECOMILLADO cuya comilla de cierre esta en una linea
170
+ // posterior (`description: "hola` / ` mundo"`), que sin esto devolvia
171
+ // `"hola` — con la comilla de apertura pegada — como descripcion.
172
+ // Sin juntar las lineas primero, del lado escritor ademas quedaban
173
+ // huerfanas en la salida exportada.
174
+ let endLine = startLine;
175
+ let value = raw;
176
+ const opensUnclosedQuote = /^['"]/.test(raw) && findClosingQuote(raw, raw[0]) === -1;
177
+ if (raw !== '' && (!/^['"]/.test(raw) || opensUnclosedQuote)) {
178
+ const cont = [];
179
+ for (let i = startLine + 1; i < lines.length; i++) {
180
+ if (lines[i].trim() === '')
181
+ break;
182
+ if (!/^[ \t]/.test(lines[i]))
183
+ break;
184
+ cont.push(lines[i].trim());
185
+ endLine = i;
186
+ }
187
+ if (cont.length > 0)
188
+ value = [raw, ...cont].join(' ');
189
+ }
190
+ // Escalares entrecomillados. Dos cosas que no alcanzan por separado:
191
+ // 1. Hay que ENCONTRAR la comilla de cierre real antes de mirar el resto:
192
+ // lo que sigue a esa comilla es un comentario, no parte del valor.
193
+ // Comparar con `endsWith` fallaba justo cuando habia comentario
194
+ // (`description: "x" # nota` devolvia `"x"` CON las comillas literales,
195
+ // mientras js-yaml devuelve `x`).
196
+ // 2. Hay que deshacer el escape propio de cada estilo: single-quoted
197
+ // duplica el apostrofe (`''`); double-quoted es superset de la string
198
+ // JSON, asi que JSON.parse resuelve \n, \t, \", \\ y \uXXXX.
199
+ const quote = value[0];
200
+ if (quote === "'" || quote === '"') {
201
+ const close = findClosingQuote(value, quote);
202
+ if (close !== -1) {
203
+ const scalar = value.slice(0, close + 1);
204
+ if (quote === "'") {
205
+ return { value: scalar.slice(1, -1).replace(/''/g, "'").trim(), startLine, endLine };
206
+ }
207
+ try {
208
+ return { value: String(JSON.parse(scalar)).trim(), startLine, endLine };
209
+ }
210
+ catch {
211
+ return { value: scalar.slice(1, -1).trim(), startLine, endLine };
212
+ }
213
+ }
214
+ }
215
+ // Escalar plano: un `#` PRECEDIDO DE ESPACIO abre un comentario y no forma
216
+ // parte del valor (un `#` pegado a texto, como en `C#`, si). Dentro de
217
+ // comillas el `#` es literal, y esas ramas ya retornaron.
218
+ return { value: value.replace(/\s+#.*$/, '').trim(), startLine, endLine };
219
+ }
220
+ /** Azucar para el caso comun: solo el valor resuelto de `description`. */
221
+ function readFrontmatterDescription(frontmatter) {
222
+ return findFrontmatterDescription(frontmatter).value;
223
+ }
@@ -1,21 +1,52 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.parseCanonicalAgent = parseCanonicalAgent;
4
+ const frontmatter_1 = require("../frontmatter");
4
5
  function parseCanonicalAgent(source) {
5
6
  const match = source.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/);
6
7
  if (!match)
7
8
  throw new Error('canonical agent requires YAML frontmatter');
8
9
  const fields = new Map();
9
- for (const line of match[1].split(/\r?\n/)) {
10
+ // La validacion linea-por-linea sigue siendo estricta a proposito (atrapa
11
+ // frontmatter malformado), pero debe reconocer los block scalars: sus
12
+ // lineas de CONTENIDO estan indentadas y no son pares `clave: valor`.
13
+ // Antes lanzaban "invalid canonical agent frontmatter line", asi que un
14
+ // agente con `description: >-` rompia `awm add <x> -a codex`. Peor aun
15
+ // tras unificar el discovery: el picker mostraba la descripcion bien
16
+ // resuelta y recien despues explotaba el install — dos caminos leyendo el
17
+ // MISMO archivo con reglas distintas.
18
+ const lines = match[1].split(/\r?\n/);
19
+ let sawKey = false;
20
+ for (const line of lines) {
21
+ // Toda linea INDENTADA (o en blanco) es continuacion del valor de la
22
+ // clave anterior, no una clave nueva: asi se ven tanto el contenido de
23
+ // un block scalar como el de un escalar plano multilinea, y ambos son
24
+ // YAML valido. El primer fix de esto solo contemplo el block scalar,
25
+ // asi que `description: primera\n continuacion` seguia lanzando
26
+ // "invalid canonical agent frontmatter line" y rompiendo
27
+ // `awm add -a codex` — mientras discovery, leyendo el MISMO archivo con
28
+ // la funcion compartida, mostraba la descripcion correcta en el picker.
29
+ // Dos caminos sobre el mismo archivo o se arreglan juntos o divergen de
30
+ // una forma mas confusa que el bug original.
31
+ if (sawKey && (line.trim() === '' || /^[ \t]/.test(line)))
32
+ continue;
10
33
  const field = line.match(/^([A-Za-z][A-Za-z0-9_-]*):\s*(.*)$/);
11
34
  if (!field)
12
35
  throw new Error(`invalid canonical agent frontmatter line: ${line}`);
13
36
  if (fields.has(field[1]))
14
37
  throw new Error(`duplicate canonical agent frontmatter key: ${field[1]}`);
38
+ sawKey = true;
39
+ if ((0, frontmatter_1.isBlockScalarHeader)(field[2])) {
40
+ fields.set(field[1], '');
41
+ continue;
42
+ }
15
43
  fields.set(field[1], field[2].replace(/^(['"])(.*)\1$/, '$2').trim());
16
44
  }
17
45
  const name = fields.get('name') ?? '';
18
- const description = fields.get('description') ?? '';
46
+ // La descripcion sale de la funcion compartida (resuelve block scalars y
47
+ // deshace el escape de los escalares entrecomillados) — nunca del mapa de
48
+ // arriba, que solo corta comillas.
49
+ const description = (0, frontmatter_1.readFrontmatterDescription)(match[1]);
19
50
  const instructions = match[2].trim();
20
51
  if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name))
21
52
  throw new Error('invalid agent name');
@@ -10,7 +10,7 @@ exports.parseSkillSource = parseSkillSource;
10
10
  // link. Reuses discovery.ts's `matchFrontmatterBlock` (already the single
11
11
  // source of truth for locating the frontmatter block elsewhere in this
12
12
  // codebase) rather than writing a second frontmatter parser.
13
- const discovery_1 = require("../discovery");
13
+ const frontmatter_1 = require("../frontmatter");
14
14
  /**
15
15
  * Parses a raw SKILL.md source into its `description` frontmatter field and
16
16
  * body content. Mirrors discovery.ts's `readArtifactDescription` for the
@@ -20,26 +20,18 @@ const discovery_1 = require("../discovery");
20
20
  * empty description or body.
21
21
  */
22
22
  function parseSkillSource(source) {
23
- const frontmatter = (0, discovery_1.matchFrontmatterBlock)(source);
23
+ const frontmatter = (0, frontmatter_1.matchFrontmatterBlock)(source);
24
24
  if (frontmatter === null)
25
25
  throw new Error('skill source requires YAML frontmatter');
26
- const line = frontmatter.split(/\r?\n/).find((l) => /^description\s*:/.test(l));
27
- if (!line)
28
- throw new Error('skill source requires a non-empty description');
29
- let description = line.replace(/^description\s*:/, '').trim();
30
- if ((description.startsWith('"') && description.endsWith('"')) ||
31
- (description.startsWith("'") && description.endsWith("'"))) {
32
- description = description.slice(1, -1);
33
- }
34
- // A YAML block scalar indicator (`>-`, `|-`, `>`, `|`, `>+`, `|+`) means the
35
- // real description text lives on the FOLLOWING indented lines, not on this
36
- // line at all — treating the bare indicator as the description would embed
37
- // literal "|-" into every rendered skill. Mirrors discovery.ts's
38
- // readArtifactDescription, which detects the same shape and treats it as
39
- // absent rather than mis-parsing it.
40
- const BLOCK_INDICATORS = new Set(['>-', '>', '|-', '|', '>+', '|+']);
41
- if (BLOCK_INDICATORS.has(description))
42
- description = '';
26
+ // Delega en readFrontmatterDescription (discovery.ts) la MISMA funcion
27
+ // que usa el discovery del CLI, incluyendo la resolucion de block scalars
28
+ // (`description: >-` + lineas indentadas). Antes esta funcion tenia su
29
+ // propia copia de la logica, y ambas colapsaban el indicador de block
30
+ // scalar a '' en vez de leer el texto de las lineas siguientes: eso
31
+ // crasheaba `awm add <bundle> -a copilot|cursor` contra un skill real del
32
+ // registry baseline. Una sola implementacion = no pueden volver a
33
+ // divergir.
34
+ const description = (0, frontmatter_1.readFrontmatterDescription)(frontmatter);
43
35
  if (!description)
44
36
  throw new Error('skill source requires a non-empty description');
45
37
  const bodyMatch = source.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n([\s\S]*)$/);
@@ -30,11 +30,19 @@ function displayWidth(s) {
30
30
  }
31
31
  return w;
32
32
  }
33
- /** Truncate plain text to `width` cells, appending '…'. Strips ANSI — color AFTER truncating. */
33
+ /** Truncate plain text to `width` cells, appending '…'. Strips ANSI — color AFTER truncating.
34
+ *
35
+ * Los saltos de linea se colapsan a un espacio: esto se usa para celdas de UNA
36
+ * linea (filas de `awm list`), y `displayWidth` cuenta un `\n` como ancho 1 en
37
+ * vez de reconocerlo como salto — o sea que una string multilinea podia pasar
38
+ * el chequeo de ancho y emitirse cruda, partiendo la fila y desalineando todo
39
+ * lo que viniera despues. Antes era inalcanzable; dejo de serlo cuando el
40
+ * lector de frontmatter aprendio a resolver block scalars literales (`|`), que
41
+ * producen saltos de linea REALES en la descripcion. */
34
42
  function truncate(s, width) {
35
43
  if (width <= 0)
36
44
  return '';
37
- const plain = stripAnsi(s);
45
+ const plain = stripAnsi(s).replace(/\s*\r?\n\s*/g, ' ');
38
46
  if (displayWidth(plain) <= width)
39
47
  return plain;
40
48
  if (width === 1)
@@ -105,8 +105,17 @@ describe('Artifact Discovery', () => {
105
105
  fs_1.default.readFileSync.mockImplementation(() => { throw new Error('ENOENT'); });
106
106
  expect((0, discovery_1.readArtifactDescription)('/missing/SKILL.md')).toBe('');
107
107
  });
108
- it('returns empty string for a block scalar description indicator', () => {
108
+ it('resuelve un block scalar leyendo sus lineas indentadas (antes descartaba el texto y devolvia vacio)', () => {
109
+ // Este test afirmaba `''` sobre un fixture que contiene literalmente
110
+ // "actual text" — codificaba la degradacion silenciosa, no un
111
+ // contrato. Un `description: >-` es YAML valido y su texto vive en
112
+ // las lineas indentadas siguientes; descartarlo dejaba el skill sin
113
+ // descripcion en todo el discovery del CLI.
109
114
  fs_1.default.readFileSync.mockReturnValue('---\ndescription: >-\n actual text\n---\n');
115
+ expect((0, discovery_1.readArtifactDescription)('/any/SKILL.md')).toBe('actual text');
116
+ });
117
+ it('sigue devolviendo vacio si el indicador de block scalar no tiene lineas indentadas (genuinamente sin descripcion)', () => {
118
+ fs_1.default.readFileSync.mockReturnValue('---\ndescription: >-\nname: otra-clave\n---\n');
110
119
  expect((0, discovery_1.readArtifactDescription)('/any/SKILL.md')).toBe('');
111
120
  });
112
121
  });
@@ -1,7 +1,24 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ const js_yaml_1 = __importDefault(require("js-yaml"));
3
7
  const transform_1 = require("../../../src/core/export/transform");
4
8
  const FM = (lines) => `---\n${lines.join('\n')}\n---\nBody line.\n`;
9
+ /** Reparsea el frontmatter EXPORTADO con un YAML real y devuelve su
10
+ * `description`. Lanza si la salida no es YAML valido — que es justamente lo
11
+ * que queremos que falle fuerte, en vez de asertar sobre el texto crudo y no
12
+ * enterarnos de que emitimos algo que ningun parser puede leer. */
13
+ function descriptionOf(exported) {
14
+ const fm = exported.match(/^---\r?\n([\s\S]*?)\r?\n---/);
15
+ if (!fm)
16
+ throw new Error('la salida no tiene bloque de frontmatter');
17
+ const parsed = js_yaml_1.default.load(fm[1]);
18
+ if (typeof parsed.description !== 'string')
19
+ throw new Error('description ausente o no-string en la salida');
20
+ return parsed.description;
21
+ }
5
22
  describe('claudeAiTransform', () => {
6
23
  it('strips version and portable, keeps other keys and body intact', () => {
7
24
  const input = FM(['name: mermaid-diagrams', 'version: "1.0.0"', 'portable: true', 'description: "Guide."']);
@@ -16,18 +33,25 @@ describe('claudeAiTransform', () => {
16
33
  const out = (0, transform_1.claudeAiTransform)(input, 'x');
17
34
  expect(out).toContain(`description: "Does things. ${(0, transform_1.DEFERENCE_LINE)('x')}"`);
18
35
  });
36
+ // NOTA sobre el estilo de la salida: el transform ya no conserva el estilo de
37
+ // comillas del fuente — emite SIEMPRE un escalar double-quoted via
38
+ // JSON.stringify, sea cual sea la forma de entrada (plano, entrecomillado,
39
+ // block scalar, multilinea). Antes habia una rama por forma, y cada vez que
40
+ // el lector aprendia una forma nueva el escritor quedaba atras: asi se
41
+ // colaron una deference line enterrada en medio de una descripcion
42
+ // multilinea y otra tragada entera por un `#` de comentario. Un solo camino
43
+ // hace imposible esa clase de divergencia. El contrato que estos tests
44
+ // protegen — YAML valido que conserva descripcion + deference line — se
45
+ // cumple igual, y se verifica reparseando la salida con js-yaml mas abajo.
19
46
  it('appends the deference line to an unquoted description', () => {
20
47
  const input = FM(['name: x', 'portable: true', 'description: Does things.']);
21
48
  const out = (0, transform_1.claudeAiTransform)(input, 'x');
22
- expect(out).toContain(`description: Does things. ${(0, transform_1.DEFERENCE_LINE)('x')}`);
49
+ expect(out).toContain(`description: ${JSON.stringify(`Does things. ${(0, transform_1.DEFERENCE_LINE)('x')}`)}`);
23
50
  });
24
51
  it('appends the deference line inside a single-quoted description', () => {
25
52
  const input = FM(['name: x', 'portable: true', "description: 'Does things.'"]);
26
53
  const out = (0, transform_1.claudeAiTransform)(input, 'x');
27
- // DEFERENCE_LINE itself always contains an apostrophe ("registry's"), so
28
- // even a fixture with no apostrophe of its own must see it doubled ('')
29
- // per YAML single-quote escaping once spliced into a single-quoted scalar.
30
- expect(out).toContain(`description: 'Does things. ${(0, transform_1.DEFERENCE_LINE)('x').replace(/'/g, "''")}'`);
54
+ expect(out).toContain(`description: ${JSON.stringify(`Does things. ${(0, transform_1.DEFERENCE_LINE)('x')}`)}`);
31
55
  });
32
56
  it('appends the deference line inside a double-quoted description with trailing whitespace', () => {
33
57
  const input = FM(['name: x', 'portable: true', 'description: "Does things." ']);
@@ -50,26 +74,91 @@ describe('claudeAiTransform', () => {
50
74
  it('throws on frontmatter without description', () => {
51
75
  expect(() => (0, transform_1.claudeAiTransform)(FM(['name: x', 'portable: true']), 'x')).toThrow(/description/);
52
76
  });
53
- it('throws on multi-line (block scalar) description', () => {
54
- expect(() => (0, transform_1.claudeAiTransform)(FM(['name: x', 'description: >', ' folded text']), 'x')).toThrow(/single-line/);
77
+ it('resuelve una descripcion en block scalar y le anexa la deference line (antes abortaba el export del bundle entero)', () => {
78
+ // Regresion real: esto lanzaba, y runExport propaga el throw — asi que UN
79
+ // skill del registry baseline con esta forma valida (extract-design-md)
80
+ // hacia fallar `awm export frontend` COMPLETO, no solo ese skill.
81
+ const out = (0, transform_1.claudeAiTransform)(FM(['name: x', 'description: >', ' folded text', ' segunda linea']), 'x');
82
+ const descLine = out.split('\n').find((l) => l.startsWith('description:'));
83
+ expect(descLine).toBe(`description: ${JSON.stringify(`folded text segunda linea ${(0, transform_1.DEFERENCE_LINE)('x')}`)}`);
84
+ // Las lineas indentadas del bloque se consumieron: no quedan sueltas.
85
+ expect(out).not.toContain(' folded text');
86
+ expect(out).not.toContain('description: >');
87
+ });
88
+ it('no absorbe las claves siguientes del frontmatter al consumir el bloque', () => {
89
+ const out = (0, transform_1.claudeAiTransform)(FM(['description: >-', ' solo esto', 'name: sigue-viva']), 'x');
90
+ expect(out).toContain('name: sigue-viva');
91
+ const descLine = out.split('\n').find((l) => l.startsWith('description:'));
92
+ expect(descLine).toContain('solo esto');
93
+ expect(descLine).not.toContain('sigue-viva');
94
+ });
95
+ it('sigue lanzando si el block scalar no tiene contenido', () => {
96
+ expect(() => (0, transform_1.claudeAiTransform)(FM(['name: x', 'description: >-']), 'x')).toThrow(/no content/);
97
+ });
98
+ it('lanza si description esta presente pero vacia (sin valor y sin bloque)', () => {
99
+ expect(() => (0, transform_1.claudeAiTransform)(FM(['name: x', 'description:']), 'x')).toThrow(/description is empty/);
55
100
  });
56
- it('escapes an apostrophe in the deference line when appending to a single-quoted description', () => {
101
+ it('resuelve un block scalar CON comentario final sin perder el contenido (regresion: guarda por prefijo vs match completo)', () => {
102
+ // La guarda de la rama de bloque usaba un prefijo (/^[>|]/) mientras el
103
+ // resolver exigia match COMPLETO del indicador. Con `>- # nota` entraban en
104
+ // desacuerdo: se publicaba el indicador como descripcion y las lineas de
105
+ // contenido REALES se borraban del artefacto exportado, en silencio.
106
+ const out = (0, transform_1.claudeAiTransform)(FM(['name: x', 'description: >- # nota al margen', ' el texto real']), 'x');
107
+ const descLine = out.split('\n').find((l) => l.startsWith('description:'));
108
+ expect(descLine).toContain('el texto real');
109
+ expect(descLine).not.toContain('# nota al margen');
110
+ expect(out).not.toContain('description: >-');
111
+ });
112
+ it('lanza ante un indicador de bloque malformado en vez de emitir YAML invalido', () => {
113
+ // `>-basura` lo rechaza el propio YAML. Tratarlo como escalar plano emitiria
114
+ // `description: >-basura ...`, invalido porque `>` abre un indicador.
115
+ expect(() => (0, transform_1.claudeAiTransform)(FM(['name: x', 'description: >-basura', ' texto']), 'x'))
116
+ .toThrow(/malformed block scalar indicator/);
117
+ });
118
+ it('conserva la linea en blanco que separa el bloque de la clave siguiente', () => {
119
+ const out = (0, transform_1.claudeAiTransform)(FM(['description: >-', ' el texto', '', 'name: x']), 'x');
120
+ expect(out).toMatch(/description: .*\n\nname: x/);
121
+ });
122
+ it('no deja que el apostrofe de la deference line rompa el escalar emitido', () => {
123
+ // DEFERENCE_LINE siempre contiene un apostrofe ("registry's"). El fix
124
+ // original lo doblaba ('') porque la salida era single-quoted; hoy la
125
+ // salida es double-quoted, donde el apostrofe es un caracter comun. Lo
126
+ // que el test protege no es la convencion de comillas sino que el
127
+ // resultado sea YAML bien formado con el texto intacto — se verifica
128
+ // reparseando con js-yaml en vez de inspeccionar comillas a ojo.
57
129
  const input = FM(['name: mermaid', 'portable: true', "description: 'Diagrams and flowcharts.'"]);
58
130
  const out = (0, transform_1.claudeAiTransform)(input, 'mermaid');
59
- // The apostrophe in "registry's" must be doubled ('') per YAML single-quote escaping.
60
- expect(out).toContain("registry''s mermaid skill");
61
- expect(out).not.toContain("registry's mermaid skill");
62
- const descLine = out.split('\n').find((l) => l.startsWith('description:'));
63
- expect(descLine).toBe(`description: 'Diagrams and flowcharts. ${(0, transform_1.DEFERENCE_LINE)('mermaid').replace(/'/g, "''")}'`);
64
- // Sanity check the result is well-formed: single-quoted scalar body has no
65
- // lone (unescaped) apostrophes every ' is either the opening/closing
66
- // quote or part of a doubled '' pair.
67
- const body = descLine.slice('description: \''.length, -1);
68
- expect(body.replace(/''/g, '')).not.toMatch(/'/);
69
- });
70
- it('throws when a quoted description has trailing content after its closing quote (e.g. inline comment)', () => {
131
+ expect(descriptionOf(out)).toBe(`Diagrams and flowcharts. ${(0, transform_1.DEFERENCE_LINE)('mermaid')}`);
132
+ expect(descriptionOf(out)).toContain("registry's mermaid skill");
133
+ });
134
+ it('resuelve un escalar entrecomillado con comentario final en vez de lanzar', () => {
135
+ // Antes lanzaba. Pero `description: "x" # nota` es YAML valido y js-yaml lo
136
+ // lee como "x": el comentario no forma parte del valor. Lanzar obligaba al
137
+ // autor a tocar un SKILL.md que no tenia nada malo.
71
138
  const input = FM(['name: x', 'portable: true', 'description: "Does things." # a comment']);
72
- expect(() => (0, transform_1.claudeAiTransform)(input, 'x')).toThrow(/trailing content|comment/i);
139
+ expect(descriptionOf((0, transform_1.claudeAiTransform)(input, 'x'))).toBe(`Does things. ${(0, transform_1.DEFERENCE_LINE)('x')}`);
140
+ });
141
+ describe('round-trip: la salida se reparsea con un YAML real (par lector+escritor)', () => {
142
+ // Los tests del lector, por si solos, no habrian detectado que el ESCRITOR
143
+ // quedaba atras cuando el lector aprendia una forma nueva. Estos casos
144
+ // pasan cada forma por el transform y REPARSEAN el resultado, que es donde
145
+ // se ve si la deference line sobrevivio y si el YAML sigue siendo valido.
146
+ it.each([
147
+ ['plano', ['description: Does things.']],
148
+ ['plano multilinea', ['description: primera parte', ' y su continuacion']],
149
+ ['plano con comentario final', ['description: hola mundo # nota del autor']],
150
+ ['single-quoted', ["description: 'con apostrofe: it''s'"]],
151
+ ['double-quoted', ['description: "con dos puntos: si"']],
152
+ ['folded', ['description: >-', ' primera', ' segunda']],
153
+ ['literal', ['description: |-', ' primera', ' segunda']],
154
+ ['folded con comentario en el indicador', ['description: >- # nota', ' el texto']],
155
+ ])('%s', (_name, descLines) => {
156
+ const out = (0, transform_1.claudeAiTransform)(FM(['name: x', 'portable: true', ...descLines]), 'x');
157
+ const desc = descriptionOf(out); // lanza si la salida no es YAML valido
158
+ // La deference line es la razon de existir de este transform: nunca puede
159
+ // perderse en un comentario ni quedar sepultada en el medio del texto.
160
+ expect(desc.endsWith((0, transform_1.DEFERENCE_LINE)('x'))).toBe(true);
161
+ });
73
162
  });
74
163
  it('cleans intra-registry paths in the body', () => {
75
164
  const md = [
@@ -0,0 +1,118 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ // Test DIFERENCIAL: readFrontmatterDescription (parser a mano, cero deps de
7
+ // runtime) contra js-yaml (parser YAML real, devDependency solo de test).
8
+ //
9
+ // Por que existe: el repo parsea el frontmatter a mano a proposito (ver
10
+ // core/export/transform.ts — "sin parser YAML a proposito, YAGNI, cero deps").
11
+ // Esa decision es sostenible SOLO si algo verifica que la implementacion a
12
+ // mano coincide con la semantica YAML real; si no, cada forma valida que el
13
+ // parser casero no contempla es un bug esperando a un skill que la use. Ya
14
+ // paso dos veces: (1) un block scalar (`description: >-`) se colapsaba a
15
+ // vacio, rompiendo `awm add -a cursor|copilot` y abortando `awm export`
16
+ // entero contra un skill real del registry; (2) este mismo test diferencial
17
+ // descubrio que un escalar single-quoted no deshacia el escape YAML de `'`
18
+ // (duplicado: `''`), devolviendo "it''s" en vez de "it's".
19
+ //
20
+ // js-yaml es devDependency: NO entra en el bundle que instalan los usuarios.
21
+ // La regla de cero dependencias de runtime queda intacta — de hecho este test
22
+ // es lo que la hace defendible.
23
+ const js_yaml_1 = __importDefault(require("js-yaml"));
24
+ const discovery_1 = require("../../src/core/discovery");
25
+ /** Cada caso es un bloque de frontmatter cuyo `description` debe resolverse
26
+ * IGUAL que como lo hace un parser YAML real. Sin valores esperados
27
+ * hardcodeados a proposito: la referencia es js-yaml, asi que el test no
28
+ * puede "envejecer" hacia una expectativa equivocada. */
29
+ const CASES = [
30
+ ['folded (>)', 'description: >\n a\n b'],
31
+ ['folded strip (>-)', 'description: >-\n a\n b'],
32
+ ['folded keep (>+)', 'description: >+\n a\n b'],
33
+ ['literal (|)', 'description: |\n a\n b'],
34
+ ['literal strip (|-)', 'description: |-\n a\n b'],
35
+ ['folded con parrafos', 'description: >-\n a\n\n b'],
36
+ ['literal con parrafos', 'description: |-\n a\n\n b'],
37
+ ['bloque seguido de otra clave', 'description: >-\n solo\nname: x'],
38
+ ['bloque con indent 4', 'description: >-\n a\n b'],
39
+ ['bloque con indent explicito', 'description: >2\n a\n b'],
40
+ ['bloque de una sola linea', 'description: >-\n sola'],
41
+ ['bloque con colon adentro', 'description: >-\n clave: valor'],
42
+ ['bloque con hash adentro', 'description: >-\n a #no-es-comentario'],
43
+ ['plain', 'description: hola mundo'],
44
+ ['plain con guion', 'description: usa - guiones'],
45
+ ['double-quoted con colon', 'description: "hola: mundo"'],
46
+ ['double-quoted con comilla escapada', 'description: "dice \\"hola\\""'],
47
+ ['double-quoted con \\n', 'description: "a\\nb"'],
48
+ ['double-quoted con backslash', 'description: "c:\\\\\\\\ruta"'],
49
+ ['single-quoted con apostrofe escapado', "description: 'it''s aqui'"],
50
+ ['single-quoted con doble apostrofe', "description: 'a''''b'"],
51
+ ['single-quoted con colon', "description: 'clave: valor'"],
52
+ ['double-quoted con em-dash', 'description: "em — dash"'],
53
+ // Casos que una revision adversarial encontro y los tests a mano no cubrian.
54
+ ['folded con lineas MAS indentadas (no se pliegan)', 'description: >-\n intro\n - item uno\n - item dos\n outro'],
55
+ ['folded con indentacion irregular', 'description: >\n foo\n bar\n baz'],
56
+ ['indicador con comentario final', 'description: >- # nota al margen\n el texto real'],
57
+ ['literal con comentario final', 'description: |- # nota\n linea'],
58
+ ['clave description indentada NO gana sobre la real', 'metadata:\n description: nota interna\ndescription: la verdadera'],
59
+ ['description dentro del contenido de otro bloque', 'example: |\n description: falsa\ndescription: la verdadera'],
60
+ ['plain con comentario final', 'description: hola mundo # un comentario'],
61
+ ['plain con hash pegado (NO es comentario)', 'description: usar C# y F#'],
62
+ ['plain multilinea (se pliega como folded)', 'description: primera\n segunda\n tercera'],
63
+ ['quoted con hash adentro (literal)', 'description: "tiene # adentro"'],
64
+ // Segunda tanda de revision adversarial: espacios, blancos consecutivos y
65
+ // escalares entrecomillados que cierran en una linea posterior.
66
+ ['blancos consecutivos en folded', 'description: >-\n a\n\n\n b'],
67
+ ['espacios finales dentro de un folded', 'description: >-\n a \n b'],
68
+ ['linea solo-espacios dentro de un folded', 'description: >-\n a\n \n b'],
69
+ ['double-quoted multilinea', 'description: "hola\n mundo"'],
70
+ ['single-quoted multilinea', "description: 'hola\n mundo'"],
71
+ ['double-quoted con comentario final', 'description: "x" # comentario'],
72
+ ['single-quoted con comentario final', "description: 'y' # comentario"],
73
+ ['single-quoted con apostrofe y comentario', "description: 'it''s aqui' # nota"],
74
+ ];
75
+ /** Formas que YAML RECHAZA: no hay valor correcto que devolver, y devolver el
76
+ * indicador crudo como si fuera la descripcion seria peor que no devolver
77
+ * nada (se publicaria en el artefacto). El contrato aca es '' — que hace
78
+ * fallar fuerte a parseSkillSource y claudeAiTransform. */
79
+ const INVALID_CASES = [
80
+ ['indicador con indent 0', 'description: >0\n a'],
81
+ ['indicador con indent de 2 digitos', 'description: >12\n a'],
82
+ ['indicador con basura pegada', 'description: >-basura\n a'],
83
+ ['indicador con comentario sin espacio', 'description: >-#c\n a'],
84
+ ];
85
+ describe('readFrontmatterDescription coincide con un parser YAML real (js-yaml)', () => {
86
+ it.each(CASES)('%s', (_name, frontmatter) => {
87
+ const parsed = js_yaml_1.default.load(frontmatter);
88
+ // Guard del propio fixture: si js-yaml no lo lee como string, el caso
89
+ // esta mal escrito y el test seria vacuo — que falle fuerte.
90
+ expect(typeof parsed.description).toBe('string');
91
+ const expected = parsed.description.trim();
92
+ expect((0, discovery_1.readFrontmatterDescription)(frontmatter)).toBe(expected);
93
+ });
94
+ it.each(INVALID_CASES)('%s: YAML lo rechaza, nosotros devolvemos vacio (nunca el indicador crudo)', (_name, frontmatter) => {
95
+ // Guard del fixture: si js-yaml LO ACEPTARA, el caso esta mal clasificado
96
+ // y este test seria vacuo — que falle fuerte.
97
+ expect(() => js_yaml_1.default.load(frontmatter)).toThrow();
98
+ expect((0, discovery_1.readFrontmatterDescription)(frontmatter)).toBe('');
99
+ });
100
+ it('resuelve el SKILL.md real del registry igual que js-yaml (extract-design-md, el caso que origino el bug)', () => {
101
+ // Forma exacta del skill real de awm-baseline-registry que rompia
102
+ // `awm add -a cursor|copilot` y `awm export frontend`.
103
+ const frontmatter = [
104
+ 'name: extract-design-md',
105
+ 'version: "1.0.1"',
106
+ 'description: >-',
107
+ ' Extract a comprehensive design system (DESIGN.md) directly from frontend source',
108
+ ' code — React, Vue, Svelte, Angular, plain HTML/CSS, or any web framework.',
109
+ 'allowed-tools:',
110
+ ' - "Bash"',
111
+ ].join('\n');
112
+ const expected = (js_yaml_1.default.load(frontmatter).description).trim();
113
+ expect((0, discovery_1.readFrontmatterDescription)(frontmatter)).toBe(expected);
114
+ // Y no absorbio la clave siguiente ni sus items indentados.
115
+ expect((0, discovery_1.readFrontmatterDescription)(frontmatter)).not.toContain('allowed-tools');
116
+ expect((0, discovery_1.readFrontmatterDescription)(frontmatter)).not.toContain('Bash');
117
+ });
118
+ });
@@ -46,6 +46,42 @@ it('ignores provider-only mode while retaining canonical instructions', () => {
46
46
  instructions: expect.stringContaining('You do NOT write code directly.'),
47
47
  }); // verifies R8, R9
48
48
  });
49
+ it('acepta una description en block scalar y la resuelve (regresion: rompia awm add -a codex)', () => {
50
+ // El parser exigia que TODA linea del frontmatter fuera `clave: valor`, asi
51
+ // que las lineas indentadas de un bloque lanzaban "invalid canonical agent
52
+ // frontmatter line" y abortaban el install de codex. Peor: discovery lee el
53
+ // MISMO archivo y (ya arreglado) mostraba la descripcion bien en el picker,
54
+ // asi que el crash llegaba despues de que la UI dijera que todo estaba OK.
55
+ const source = [
56
+ '---',
57
+ 'name: bloque-agente',
58
+ 'description: >-',
59
+ ' Primera linea de la descripcion',
60
+ ' y su continuacion.',
61
+ '---',
62
+ 'Cuerpo de instrucciones.',
63
+ ].join('\n');
64
+ expect((0, canonical_agent_1.parseCanonicalAgent)(source)).toEqual({
65
+ name: 'bloque-agente',
66
+ description: 'Primera linea de la descripcion y su continuacion.',
67
+ instructions: 'Cuerpo de instrucciones.',
68
+ });
69
+ });
70
+ it('acepta un escalar plano multilinea y lo pliega (js-yaml lee exactamente esto)', () => {
71
+ // Ojo: una version previa de este test exigia un throw aca. Estaba mal —
72
+ // `description: x` seguido de una linea indentada es un escalar plano
73
+ // multilinea, YAML perfectamente valido, y js-yaml lo lee como
74
+ // "x suelta e indentada". Asertar el throw codificaba una divergencia con
75
+ // YAML, no un contrato.
76
+ const source = '---\nname: ok\ndescription: x\n suelta e indentada\n---\nbody';
77
+ expect((0, canonical_agent_1.parseCanonicalAgent)(source).description).toBe('x suelta e indentada');
78
+ });
79
+ it('sigue rechazando una linea que no es ni clave ni continuacion indentada', () => {
80
+ // El fix no afloja la validacion estricta: una linea en columna 0 que no es
81
+ // `clave: valor` sigue siendo frontmatter malformado.
82
+ const source = '---\nname: ok\ndescription: x\nno soy un campo\n---\nbody';
83
+ expect(() => (0, canonical_agent_1.parseCanonicalAgent)(source)).toThrow('invalid canonical agent frontmatter line');
84
+ });
49
85
  it.each([
50
86
  ['---\nname: Bad Name\ndescription: x\n---\nbody', 'invalid agent name'],
51
87
  ['---\nname: ok\ndescription:\n---\nbody', 'non-empty description'],
@@ -49,11 +49,18 @@ Body content.
49
49
  const rendered = (0, cursor_mdc_1.renderCursorMdc)(source);
50
50
  expect(rendered).toContain('description: "*starred description"');
51
51
  });
52
- it('throws rather than embedding a literal block-scalar indicator as the description', () => {
53
- // Regression: a YAML block scalar (`description: >-` / `|-`) means the
54
- // real text lives on the FOLLOWING indented lines, not on this line —
55
- // the original code took the bare indicator itself as the description,
56
- // which would have rendered the literal string ">-" into the .mdc file.
52
+ it('resuelve un block scalar leyendo sus lineas indentadas, sin emitir el indicador literal', () => {
53
+ // Regression, en dos etapas:
54
+ // 1. La version original tomaba el indicador (`>-`) COMO la descripcion,
55
+ // lo que habria escrito el literal ">-" dentro del .mdc.
56
+ // 2. El remedio siguiente lo detectaba pero lo colapsaba a '' (=> throw),
57
+ // tratando una forma YAML valida como "descripcion ausente" — este
58
+ // test codificaba ESE comportamiento parcial. Rompia `awm add
59
+ // <bundle> -a cursor` contra un skill real del registry baseline
60
+ // (extract-design-md), que usa exactamente esta forma.
61
+ // El contrato correcto, y el que se asserta ahora: leer el texto de las
62
+ // lineas indentadas. La intencion original del test — jamas emitir el
63
+ // indicador crudo — se conserva explicitamente abajo.
57
64
  const source = `---
58
65
  name: block-skill
59
66
  description: >-
@@ -63,16 +70,27 @@ description: >-
63
70
 
64
71
  Body content.
65
72
  `;
66
- expect(() => (0, cursor_mdc_1.renderCursorMdc)(source)).toThrow('non-empty description');
73
+ const rendered = (0, cursor_mdc_1.renderCursorMdc)(source);
74
+ expect(rendered).toContain('description: This description spans multiple lines.');
75
+ expect(rendered).not.toContain('>-');
67
76
  });
68
77
  it('quotes a description containing a mid-string " #" (starts a YAML comment, truncating the rest)', () => {
69
78
  // Regression: the original YAML_UNSAFE regex only caught `#` at the START
70
79
  // of the string — a `#` preceded by whitespace ANYWHERE in a plain scalar
71
80
  // also starts a comment. Unquoted, "Use this #important skill" would
72
81
  // render as YAML that silently truncates to "Use this".
82
+ //
83
+ // El fuente esta ENTRECOMILLADO a proposito: ahi el `#` es literal y
84
+ // sobrevive al parseo, que es la unica manera de que un `#` llegue al
85
+ // renderer y haya algo que escapar. Antes el fixture usaba la forma SIN
86
+ // comillas, pero YAML dice que ahi ` #` abre un comentario — el lector lo
87
+ // trataba como texto (divergencia con cualquier parser real) y este test
88
+ // se apoyaba en esa divergencia. El contrato que el test realmente
89
+ // protege — nunca emitir un `#` sin comillas en la SALIDA — queda intacto
90
+ // y ahora se ejercita con una entrada que de verdad lo contiene.
73
91
  const source = `---
74
92
  name: hash-skill
75
- description: Use this #important skill
93
+ description: "Use this #important skill"
76
94
  ---
77
95
 
78
96
  Body content.
@@ -81,6 +99,18 @@ Body content.
81
99
  expect(rendered).toContain('description: "Use this #important skill"');
82
100
  expect(rendered).not.toContain('description: Use this #important skill');
83
101
  });
102
+ it('trata un " #" en una description SIN comillas como comentario YAML, igual que un parser real', () => {
103
+ const source = `---
104
+ name: hash-plano
105
+ description: Use this #important skill
106
+ ---
107
+
108
+ Body content.
109
+ `;
110
+ // js-yaml devuelve "Use this" para esta entrada: el comentario no es parte
111
+ // del valor. El renderer debe coincidir con esa lectura, no inventar texto.
112
+ expect((0, cursor_mdc_1.renderCursorMdc)(source)).toContain('description: Use this\n');
113
+ });
84
114
  it('quotes a description containing an embedded null byte / control character instead of emitting it raw', () => {
85
115
  // Regression: an embedded control/null byte is invalid in a YAML plain
86
116
  // scalar regardless of position — the original code's YAML_UNSAFE regex
@@ -0,0 +1,117 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ // Regresion: `description: >-` (YAML block scalar) rompia `awm add <bundle>
4
+ // -a copilot|cursor` contra un skill REAL del registry baseline
5
+ // (skills/extract-design-md/SKILL.md, que usa exactamente esta forma).
6
+ //
7
+ // Un block scalar es YAML perfectamente valido: el indicador (`>-`, `|`, ...)
8
+ // va en la linea de la clave y el texto real vive en las lineas indentadas
9
+ // que siguen. El parser trataba el indicador como si fuera el valor, lo
10
+ // detectaba como "no es una descripcion de verdad", y lo colapsaba a '' —
11
+ // que en `parseSkillSource` significa throw ("requires a non-empty
12
+ // description") y en `readArtifactDescription` significa degradar en
13
+ // silencio a descripcion vacia. Ninguno de los dos leia jamas las lineas
14
+ // siguientes, que es donde estaba el texto todo el tiempo.
15
+ const skill_source_1 = require("../../../src/core/renderers/skill-source");
16
+ const cursor_mdc_1 = require("../../../src/core/renderers/cursor-mdc");
17
+ const copilot_instructions_1 = require("../../../src/core/renderers/copilot-instructions");
18
+ // Copia fiel del frontmatter de skills/extract-design-md/SKILL.md en
19
+ // awm-baseline-registry — el caso real que dispara el bug, no un fixture
20
+ // inventado.
21
+ const realBlockScalarSkill = `---
22
+ name: extract-design-md
23
+ version: "1.0.1"
24
+ description: >-
25
+ Extract a comprehensive design system (DESIGN.md) directly from frontend source
26
+ code — React, Vue, Svelte, Angular, plain HTML/CSS, or any web framework.
27
+ ---
28
+
29
+ # Extract Design MD
30
+
31
+ Body content.
32
+ `;
33
+ describe('parseSkillSource: descripciones en block scalar (regresion real del registry)', () => {
34
+ it('lee el texto de las lineas indentadas de un `>-`, en vez de tratar el indicador como el valor', () => {
35
+ const { description } = (0, skill_source_1.parseSkillSource)(realBlockScalarSkill);
36
+ expect(description).toBe('Extract a comprehensive design system (DESIGN.md) directly from frontend source ' +
37
+ 'code — React, Vue, Svelte, Angular, plain HTML/CSS, or any web framework.');
38
+ });
39
+ it('pliega (folded, `>`) las lineas en espacios — no conserva los saltos de linea del fuente', () => {
40
+ const source = `---
41
+ name: folded
42
+ description: >
43
+ primera linea
44
+ segunda linea
45
+ ---
46
+
47
+ Body.
48
+ `;
49
+ expect((0, skill_source_1.parseSkillSource)(source).description).toBe('primera linea segunda linea');
50
+ });
51
+ it('conserva los saltos de linea de un literal (`|`) — semantica YAML distinta de `>`', () => {
52
+ const source = `---
53
+ name: literal
54
+ description: |
55
+ primera linea
56
+ segunda linea
57
+ ---
58
+
59
+ Body.
60
+ `;
61
+ expect((0, skill_source_1.parseSkillSource)(source).description).toBe('primera linea\nsegunda linea');
62
+ });
63
+ it('trata una linea en blanco dentro de un folded como separador de parrafo, no como espacio', () => {
64
+ const source = `---
65
+ name: folded-parrafos
66
+ description: >-
67
+ parrafo uno
68
+
69
+ parrafo dos
70
+ ---
71
+
72
+ Body.
73
+ `;
74
+ expect((0, skill_source_1.parseSkillSource)(source).description).toBe('parrafo uno\nparrafo dos');
75
+ });
76
+ it('no absorbe las claves siguientes del frontmatter como parte del bloque (corta en la primera linea no indentada)', () => {
77
+ const source = `---
78
+ description: >-
79
+ solo esto pertenece al bloque
80
+ name: no-soy-parte-del-bloque
81
+ version: "9.9.9"
82
+ ---
83
+
84
+ Body.
85
+ `;
86
+ expect((0, skill_source_1.parseSkillSource)(source).description).toBe('solo esto pertenece al bloque');
87
+ });
88
+ it('resuelve el bloque igual con line endings CRLF (el repo es CRLF-tolerante y corre CI en windows-latest)', () => {
89
+ const source = '---\r\nname: crlf\r\ndescription: >-\r\n linea uno\r\n linea dos\r\n---\r\n\r\nBody aqui.\r\n';
90
+ expect((0, skill_source_1.parseSkillSource)(source).description).toBe('linea uno linea dos');
91
+ });
92
+ it('sigue lanzando si el bloque esta genuinamente vacio — un indicador sin lineas indentadas NO es una descripcion', () => {
93
+ const source = `---
94
+ name: bloque-vacio
95
+ description: >-
96
+ name: otra-clave
97
+ ---
98
+
99
+ Body.
100
+ `;
101
+ expect(() => (0, skill_source_1.parseSkillSource)(source)).toThrow(/non-empty description/);
102
+ });
103
+ });
104
+ describe('renderers: el skill real del registry se renderiza para ambos providers', () => {
105
+ it('renderCursorMdc emite la descripcion real, nunca el indicador `>-` literal', () => {
106
+ const rendered = (0, cursor_mdc_1.renderCursorMdc)(realBlockScalarSkill);
107
+ expect(rendered).toContain('Extract a comprehensive design system');
108
+ // El modo de fallo que este test ancla: emitir el indicador crudo.
109
+ expect(rendered).not.toContain('description: >-');
110
+ expect(rendered).not.toContain('description: ""');
111
+ });
112
+ it('renderCopilotInstructions no crashea (no necesita la descripcion, pero parseSkillSource la exigia igual)', () => {
113
+ const rendered = (0, copilot_instructions_1.renderCopilotInstructions)(realBlockScalarSkill);
114
+ expect(rendered).toContain('applyTo: "**"');
115
+ expect(rendered).toContain('# Extract Design MD');
116
+ });
117
+ });
@@ -44,6 +44,15 @@ describe('truncate', () => {
44
44
  it('returns empty for non-positive width', () => {
45
45
  expect((0, text_1.truncate)('abc', 0)).toBe('');
46
46
  });
47
+ it('colapsa saltos de linea a un espacio — la celda es de UNA linea', () => {
48
+ // Regresion: `displayWidth` cuenta `\n` como ancho 1, asi que una string
49
+ // multilinea corta pasaba el chequeo y se emitia cruda, partiendo la fila
50
+ // de `awm list` y desalineando todo lo siguiente. Alcanzable desde que el
51
+ // lector de frontmatter resuelve block scalars literales (`|`).
52
+ expect((0, text_1.truncate)('linea uno\nlinea dos', 80)).toBe('linea uno linea dos');
53
+ expect((0, text_1.truncate)('a\r\n b', 80)).toBe('a b');
54
+ expect((0, text_1.truncate)('linea uno\nlinea dos', 12)).toBe('linea uno l…');
55
+ });
47
56
  });
48
57
  describe('wrap', () => {
49
58
  it('breaks text at word boundaries within width', () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentic-workflow-manager",
3
- "version": "3.13.4",
3
+ "version": "3.13.5",
4
4
  "main": "dist/src/index.js",
5
5
  "bin": {
6
6
  "awm": "./dist/src/index.js"
@@ -43,10 +43,12 @@
43
43
  },
44
44
  "devDependencies": {
45
45
  "@types/jest": "^30.0.0",
46
+ "@types/js-yaml": "4.0.9",
46
47
  "@types/node": "^25.3.0",
47
48
  "dependency-cruiser": "^17.4.3",
48
49
  "eslint": "^10.4.1",
49
50
  "jest": "^30.2.0",
51
+ "js-yaml": "4.1.0",
50
52
  "ts-jest": "^29.4.6",
51
53
  "typescript": "^5.9.3"
52
54
  }