agentic-workflow-manager 3.13.3 → 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.
@@ -112,19 +112,33 @@ function gatherMachine(bundles, agent = 'claude-code') {
112
112
  }
113
113
  catch { /* sin soporte de hooks → ausente */ }
114
114
  // devCore (bundle baseline) — skillsDir is null only for providers with no global skill
115
- // discovery mechanism (today: Copilot); treated the same as "nothing linked here yet".
115
+ // discovery mechanism (today: Copilot); for those, there is no global-scope devCore
116
+ // concept to satisfy at all, so it's treated as trivially satisfied (see below).
116
117
  const skillsDir = (0, providers_1.providerFor)(agent).skill.global;
117
118
  const baseline = bundles.find((b) => b.scope === 'baseline');
118
119
  let devCorePresent = false;
119
120
  let brokenLinks = [];
120
121
  if (baseline) {
121
122
  const skillNames = (0, bundles_1.resolveBundleSkills)(baseline.name, bundles);
122
- const { linked, broken } = skillsDir !== null
123
- ? classifyLinks(skillNames, skillsDir)
124
- : { linked: [], broken: [] };
125
- const absent = skillNames.filter((s) => !linked.includes(s) && !broken.includes(s));
126
- devCorePresent = skillNames.length > 0 && (linked.length + broken.length) > 0;
127
- brokenLinks = [...broken, ...absent];
123
+ if (skillsDir === null) {
124
+ // No global skill directory for this agent (today: Copilot) — there is no
125
+ // global-scope devCore/baseline-bundle concept to satisfy for it at all, so
126
+ // "N/A" is reported as satisfied (present, nothing broken) rather than
127
+ // "missing". Mirrors globalSkills' treatment just below (empty valid/
128
+ // repairable/dead when skillsDir === null). Without this, devCorePresent
129
+ // was unconditionally false here (linked/broken forced to empty arrays),
130
+ // so `machine.devCore` could never be satisfied for Copilot — stepDevCore
131
+ // (init/steps.ts) would fall through on every run and call installBundle at
132
+ // global scope, which throws (skill.global === null), rolling back the
133
+ // ENTIRE `awm init -a copilot` transaction, 100% of the time.
134
+ devCorePresent = true;
135
+ }
136
+ else {
137
+ const { linked, broken } = classifyLinks(skillNames, skillsDir);
138
+ const absent = skillNames.filter((s) => !linked.includes(s) && !broken.includes(s));
139
+ devCorePresent = skillNames.length > 0 && (linked.length + broken.length) > 0;
140
+ brokenLinks = [...broken, ...absent];
141
+ }
128
142
  // Agent-type artifacts are never shared across agents (R12/R13 —
129
143
  // install-planner.ts — unlike skills, where OpenCode and Codex both
130
144
  // resolve to ~/.agents/skills). A shared skill directory already
@@ -155,9 +169,19 @@ function gatherMachine(bundles, agent = 'claude-code') {
155
169
  }
156
170
  }
157
171
  catch { /* sin config → ningún ambient deseado */ }
158
- const installed = wanted.filter((name) => {
172
+ // Same guard as devCorePresent above: an agent with no global skill
173
+ // directory (today: Copilot) has no way to ever receive a globally-
174
+ // installed ambient bundle either — there is no global-scope "ambient"
175
+ // concept to satisfy for it at all. Without this, `installed` was forced
176
+ // to `[]` unconditionally for Copilot regardless of `wanted`, so
177
+ // stepAmbient (init/steps.ts) would treat every entry of a machine-level
178
+ // `~/.awm/config.json`'s `ambient` array as permanently missing and call
179
+ // installBundle at global scope — which throws for Copilot exactly like
180
+ // the devCore bug this file already fixes. Reported as "N/A == already
181
+ // satisfied" (installed), not "wanted but always missing".
182
+ const installed = skillsDir === null ? [...wanted] : wanted.filter((name) => {
159
183
  const skillNames = (0, bundles_1.resolveBundleSkills)(name, bundles);
160
- if (skillNames.length === 0 || skillsDir === null)
184
+ if (skillNames.length === 0)
161
185
  return false;
162
186
  const { linked } = classifyLinks(skillNames, skillsDir);
163
187
  return linked.length === skillNames.length;
@@ -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
+ }
@@ -298,11 +298,28 @@ function sleepMsSync(ms) {
298
298
  * con una pausa breve ANTES de declarar ESRCH definitivo no debilita el
299
299
  * invariante "jamas muerte sin evidencia" — un proceso genuinamente muerto
300
300
  * sigue reportando ESRCH en el reintento; esto solo absorbe el falso
301
- * negativo transitorio. Costo maximo: ~150ms, y SOLO en la rama que ya iba
302
- * a declarar "no existe" — el camino feliz (proceso vivo, exito inmediato)
303
- * no paga nada. */
304
- const PID_EXISTS_RETRY_ATTEMPTS = 3;
305
- const PID_EXISTS_RETRY_DELAY_MS = 50;
301
+ * negativo transitorio. Costo maximo, y SOLO en la rama que ya iba a
302
+ * declarar "no existe" — el camino feliz (proceso vivo, exito inmediato)
303
+ * no paga nada.
304
+ *
305
+ * R6 post-mortem #2: el presupuesto original (3 intentos, 50ms => 100ms de
306
+ * espera real) sobrevivio 2 corridas reales de windows-latest tras mergear
307
+ * este mismo mecanismo, pero una tercera corrida real (commit identico,
308
+ * ningun cambio en este archivo) volvio a fallar EL MISMO assert en EL
309
+ * MISMO test — siempre el PRIMER spawn del archivo, nunca los siguientes
310
+ * (que reusan un binario node.exe ya "calentado" por el SO/AV en ese
311
+ * proceso de test). Eso apunta a latencia de arranque en frio (primer
312
+ * spawn del job) empujando el tiempo de visibilidad de OpenProcess mas
313
+ * alla del presupuesto anterior — no una condicion de carrera nueva, la
314
+ * MISMA, con cola mas larga de lo que 100ms cubria. Presupuesto ampliado a
315
+ * 10 intentos / 100ms (hasta ~900ms de espera real) para darle margen real
316
+ * a ese arranque en frio, siguiendo cuestionando el mismo mecanismo en vez
317
+ * de reemplazarlo (systematic-debugging: 2+ fallas del mismo sintoma exacto
318
+ * primero exige ampliar el mismo remedio antes de descartar la arquitectura
319
+ * — a diferencia del patron de "cada intento revela un problema nuevo en
320
+ * otro lugar", que si justificaria cuestionar el diseño). */
321
+ const PID_EXISTS_RETRY_ATTEMPTS = 10;
322
+ const PID_EXISTS_RETRY_DELAY_MS = 100;
306
323
  function pidExistsNative(pid) {
307
324
  for (let attempt = 0; attempt < PID_EXISTS_RETRY_ATTEMPTS; attempt++) {
308
325
  try {
@@ -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)
@@ -103,6 +103,27 @@ describe('gatherContext', () => {
103
103
  // must surface as a broken/missing link — NOT a clean, skippable state.
104
104
  expect(ctx.machine.devCore.brokenLinks).toContain('development-process.toml');
105
105
  });
106
+ // Regression for the confirmed production bug: `awm init -a copilot` crashed
107
+ // 100% of the time with "machine.devCore: skill global scope is not
108
+ // supported by Copilot...", rolling back the whole init transaction.
109
+ // Copilot has no global skill directory (providers/index.ts's
110
+ // `skill.global === null`) — before this fix, devCorePresent was
111
+ // unconditionally false in that case (linked/broken forced to empty
112
+ // arrays), so `machine.devCore` could never be satisfied and stepDevCore
113
+ // (init/steps.ts) fell through to a global-scope installBundle call every
114
+ // single run, which throws for Copilot. Now it's reported as trivially
115
+ // satisfied ("N/A" == "nothing to do"), matching how `globalSkills`
116
+ // already treats the same null-skillsDir case.
117
+ it('machine: devCore is trivially satisfied (present, no broken links) for an agent with no global skill directory (copilot)', () => {
118
+ const { gatherContext } = require('../../../src/core/diagnostics/context');
119
+ const ctx = gatherContext({
120
+ cwd: tmpHome,
121
+ bundles: [bundle('dev-core', 'baseline', ['brainstorming'])],
122
+ agent: 'copilot',
123
+ });
124
+ expect(ctx.machine.devCore.present).toBe(true);
125
+ expect(ctx.machine.devCore.brokenLinks).toEqual([]);
126
+ });
106
127
  it('machine: ambient wanted read from ~/.awm/config.json, installed reflects links', () => {
107
128
  fs_1.default.mkdirSync(path_1.default.join(tmpHome, '.awm'), { recursive: true });
108
129
  fs_1.default.writeFileSync(path_1.default.join(tmpHome, '.awm', 'config.json'), JSON.stringify({ ambient: ['personal-notion'] }));
@@ -116,6 +137,29 @@ describe('gatherContext', () => {
116
137
  expect(ctx.machine.ambient.wanted).toEqual(['personal-notion']);
117
138
  expect(ctx.machine.ambient.installed).toEqual(['personal-notion']);
118
139
  });
140
+ // Regression for the SAME structural bug as the devCore fix above, just in
141
+ // the `ambient` computation a few lines below it: Copilot has no global
142
+ // skill directory (skill.global === null), so before this fix `installed`
143
+ // was forced to `[]` unconditionally regardless of `wanted`. That made
144
+ // `stepAmbient` (init/steps.ts) treat every entry in a machine-level
145
+ // `~/.awm/config.json`'s `ambient` array as permanently missing and call
146
+ // installBundle at GLOBAL scope for Copilot — which throws with the exact
147
+ // same "skill global scope is not supported by Copilot" error the devCore
148
+ // bug had, and rolls back the whole init transaction. Now `installed`
149
+ // mirrors `wanted` when skillsDir is null (N/A treated as satisfied,
150
+ // nothing to install), matching devCore's treatment above.
151
+ it('machine: ambient is trivially satisfied (installed mirrors wanted) for an agent with no global skill directory (copilot)', () => {
152
+ fs_1.default.mkdirSync(path_1.default.join(tmpHome, '.awm'), { recursive: true });
153
+ fs_1.default.writeFileSync(path_1.default.join(tmpHome, '.awm', 'config.json'), JSON.stringify({ ambient: ['personal-notion'] }));
154
+ const { gatherContext } = require('../../../src/core/diagnostics/context');
155
+ const bundles = [
156
+ bundle('dev-core', 'baseline', ['brainstorming']),
157
+ bundle('personal-notion', 'ambient', ['notion-skill']),
158
+ ];
159
+ const ctx = gatherContext({ cwd: tmpHome, bundles, agent: 'copilot' });
160
+ expect(ctx.machine.ambient.wanted).toEqual(['personal-notion']);
161
+ expect(ctx.machine.ambient.installed).toEqual(['personal-notion']);
162
+ });
119
163
  it('machine: contextInjection empty when opencode config is absent', () => {
120
164
  const { gatherContext } = require('../../../src/core/diagnostics/context');
121
165
  const ctx = gatherContext({ cwd: tmpHome, bundles: [] });