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.
- package/dist/src/core/diagnostics/context.js +33 -9
- package/dist/src/core/discovery.js +13 -21
- package/dist/src/core/export/transform.js +30 -25
- package/dist/src/core/frontmatter.js +223 -0
- package/dist/src/core/journal/process.js +22 -5
- package/dist/src/core/renderers/canonical-agent.js +33 -2
- package/dist/src/core/renderers/skill-source.js +11 -19
- package/dist/src/ui/text.js +10 -2
- package/dist/tests/core/diagnostics/context.test.js +44 -0
- package/dist/tests/core/discovery.test.js +10 -1
- package/dist/tests/core/export/transform.test.js +110 -21
- package/dist/tests/core/frontmatter-description-vs-yaml.test.js +118 -0
- package/dist/tests/core/init/steps.test.js +55 -0
- package/dist/tests/core/journal/process.test.js +54 -1
- package/dist/tests/core/renderers/canonical-agent.test.js +36 -0
- package/dist/tests/core/renderers/cursor-mdc.test.js +37 -7
- package/dist/tests/core/renderers/skill-source-block-scalar.test.js +117 -0
- package/dist/tests/core/sync-gates.test.js +28 -3
- package/dist/tests/integration/copilot-init-isolated.test.js +203 -0
- package/dist/tests/ui/text.test.js +9 -0
- package/package.json +3 -1
|
@@ -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);
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
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
|
-
|
|
77
|
-
|
|
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
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
86
|
-
|
|
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
|
-
|
|
95
|
-
|
|
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
|
|
302
|
-
*
|
|
303
|
-
* no paga nada.
|
|
304
|
-
|
|
305
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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,
|
|
23
|
+
const frontmatter = (0, frontmatter_1.matchFrontmatterBlock)(source);
|
|
24
24
|
if (frontmatter === null)
|
|
25
25
|
throw new Error('skill source requires YAML frontmatter');
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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]*)$/);
|
package/dist/src/ui/text.js
CHANGED
|
@@ -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: [] });
|