agentic-workflow-manager 9.1.0 → 9.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/commands/context/index.js +47 -0
- package/dist/src/commands/process/index.js +9 -32
- package/dist/src/core/command-result.js +17 -0
- package/dist/src/core/context/provider.js +27 -18
- package/dist/src/core/orchestrators.js +24 -3
- package/dist/src/core/text.js +58 -0
- package/dist/src/index.js +2 -0
- package/dist/tests/commands/context.test.js +64 -0
- package/dist/tests/core/context/provider.test.js +49 -0
- package/dist/tests/core/orchestrators.test.js +38 -0
- package/dist/tests/core/text.test.js +29 -0
- package/dist/tests/integration/context-orchestrators-e2e.test.js +144 -0
- package/package.json +1 -1
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runContextOrchestrators = runContextOrchestrators;
|
|
4
|
+
exports.registerContextCommand = registerContextCommand;
|
|
5
|
+
const orchestrators_1 = require("../../core/orchestrators");
|
|
6
|
+
const provider_1 = require("../../core/context/provider");
|
|
7
|
+
const text_1 = require("../../core/text");
|
|
8
|
+
const command_result_1 = require("../../core/command-result");
|
|
9
|
+
function runContextOrchestrators(collected, opts) {
|
|
10
|
+
const stderr = (0, command_result_1.diagnosticsToStderr)(collected.diagnostics);
|
|
11
|
+
const composed = (0, provider_1.composedOrchestrators)(collected.declared);
|
|
12
|
+
if (opts.verify !== undefined) {
|
|
13
|
+
// opts.verify es el argumento CRUDO tal como lo tipeo el usuario; composed[].name
|
|
14
|
+
// ya paso por sanitizeDeclaredField. Sin normalizar este lado con la MISMA funcion,
|
|
15
|
+
// un usuario que tipee el nombre declarado exacto (con un caracter que el saneo
|
|
16
|
+
// quita, p.ej. `_` o un byte de control) se lleva un falso "not composed" (R3.5/R3.6).
|
|
17
|
+
const verifyNormalized = (0, text_1.sanitizeDeclaredField)(opts.verify);
|
|
18
|
+
const found = composed.some((o) => o.name === verifyNormalized);
|
|
19
|
+
if (!found) {
|
|
20
|
+
const available = composed.map((o) => o.name).join(', ') || '(none)';
|
|
21
|
+
return {
|
|
22
|
+
code: 2, stdout: '',
|
|
23
|
+
stderr: `${stderr}awm context orchestrators: "${verifyNormalized}" is not composed — available: ${available}\n`,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
return { code: 0, stdout: `"${verifyNormalized}" is composed into the session context.\n`, stderr };
|
|
27
|
+
}
|
|
28
|
+
if (opts.json) {
|
|
29
|
+
return { code: 0, stdout: `${JSON.stringify({ orchestrators: composed }, null, 2)}\n`, stderr };
|
|
30
|
+
}
|
|
31
|
+
if (composed.length === 0) {
|
|
32
|
+
return { code: 0, stdout: 'No declared orchestrators in the installed registries.\n', stderr };
|
|
33
|
+
}
|
|
34
|
+
const rows = composed
|
|
35
|
+
.map((o) => `${o.name} applies when: ${o.appliesWhen} -> ${o.terminatesTo}`)
|
|
36
|
+
.join('\n');
|
|
37
|
+
return { code: 0, stdout: `${rows}\n`, stderr };
|
|
38
|
+
}
|
|
39
|
+
function registerContextCommand(program) {
|
|
40
|
+
const context = program.command('context').description('read-only view of the context AWM delivers to every agent session');
|
|
41
|
+
context
|
|
42
|
+
.command('orchestrators')
|
|
43
|
+
.description('list the declared orchestrators as they are composed into the session context')
|
|
44
|
+
.option('--json', 'emit the composed list as JSON')
|
|
45
|
+
.option('--verify <name>', 'exit 0 only if that orchestrator is composed; 2 otherwise')
|
|
46
|
+
.action((opts) => (0, command_result_1.emit)(runContextOrchestrators((0, orchestrators_1.collectDeclaredOrchestrators)(), { json: opts.json === true, verify: opts.verify })));
|
|
47
|
+
}
|
|
@@ -4,6 +4,8 @@ exports.runProcessList = runProcessList;
|
|
|
4
4
|
exports.runProcessShow = runProcessShow;
|
|
5
5
|
exports.registerProcessCommand = registerProcessCommand;
|
|
6
6
|
const discover_1 = require("../../core/process/discover");
|
|
7
|
+
const text_1 = require("../../core/text");
|
|
8
|
+
const command_result_1 = require("../../core/command-result");
|
|
7
9
|
/** `source` es un path absoluto del filesystem local: identifica la máquina, no
|
|
8
10
|
* el proceso. Se usa internamente para diagnósticos, y se omite de toda salida
|
|
9
11
|
* destinada a ser compartida o consumida por otro programa. */
|
|
@@ -11,25 +13,8 @@ function publicView(model) {
|
|
|
11
13
|
const { source: _source, ...rest } = model;
|
|
12
14
|
return rest;
|
|
13
15
|
}
|
|
14
|
-
/** Formatea los diagnósticos de descubrimiento como líneas `warning: ...\n`
|
|
15
|
-
* listas para stderr. Compartido por list y show para no divergir el formato. */
|
|
16
|
-
function diagnosticsToStderr(diagnostics) {
|
|
17
|
-
return diagnostics.map((d) => `warning: ${d}\n`).join('');
|
|
18
|
-
}
|
|
19
|
-
/** Neutraliza bytes de control C0 (incluyendo ESC `\x1b`) de texto de body
|
|
20
|
-
* proveniente de un registry no confiable antes de escribirlo a una terminal
|
|
21
|
-
* humana. `\n` y `\t` se preservan porque son whitespace legítimo usado por
|
|
22
|
-
* la vista de texto; el resto de la vista JSON no pasa por acá — `JSON.stringify`
|
|
23
|
-
* ya escapa los caracteres de control como parte de producir JSON válido. Esto
|
|
24
|
-
* es un problema distinto y más simple que `sanitizeDashboardSource` (Dashboard,
|
|
25
|
-
* Task 5): ahí se canonicaliza ids/labels contra un vocabulario permitido; acá
|
|
26
|
-
* solo se neutraliza ANSI/control crudo antes de escribir a stdout. */
|
|
27
|
-
function stripControlChars(text) {
|
|
28
|
-
// eslint-disable-next-line no-control-regex -- necesitamos matchear C0 deliberadamente
|
|
29
|
-
return text.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '');
|
|
30
|
-
}
|
|
31
16
|
function runProcessList(discovered) {
|
|
32
|
-
const stderr = diagnosticsToStderr(discovered.diagnostics);
|
|
17
|
+
const stderr = (0, command_result_1.diagnosticsToStderr)(discovered.diagnostics);
|
|
33
18
|
if (discovered.models.length === 0) {
|
|
34
19
|
return { code: 0, stdout: 'No process models declared by the installed registries.\n', stderr };
|
|
35
20
|
}
|
|
@@ -39,7 +24,7 @@ function runProcessList(discovered) {
|
|
|
39
24
|
return { code: 0, stdout: `${rows}\n`, stderr };
|
|
40
25
|
}
|
|
41
26
|
function runProcessShow(discovered, name, json) {
|
|
42
|
-
const stderr = diagnosticsToStderr(discovered.diagnostics);
|
|
27
|
+
const stderr = (0, command_result_1.diagnosticsToStderr)(discovered.diagnostics);
|
|
43
28
|
const found = discovered.models.find((m) => m.name === name);
|
|
44
29
|
if (!found) {
|
|
45
30
|
const available = discovered.models.map((m) => m.name).join(', ') || '(none)';
|
|
@@ -52,10 +37,10 @@ function runProcessShow(discovered, name, json) {
|
|
|
52
37
|
const view = publicView(found);
|
|
53
38
|
const structure = view.body.structure
|
|
54
39
|
.map((sg) => [
|
|
55
|
-
`${stripControlChars(sg.id)} — ${stripControlChars(sg.text)}`,
|
|
56
|
-
...sg.operations.map((op) => ` ${stripControlChars(op.id)} — ${stripControlChars(op.text)}`),
|
|
40
|
+
`${(0, text_1.stripControlChars)(sg.id)} — ${(0, text_1.stripControlChars)(sg.text)}`,
|
|
41
|
+
...sg.operations.map((op) => ` ${(0, text_1.stripControlChars)(op.id)} — ${(0, text_1.stripControlChars)(op.text)}`),
|
|
57
42
|
].join('\n')).join('\n');
|
|
58
|
-
return { code: 0, stdout: `${view.name} (${view.status})\n\n${stripControlChars(view.body.objective)}\n\n${structure}\n`, stderr };
|
|
43
|
+
return { code: 0, stdout: `${view.name} (${view.status})\n\n${(0, text_1.stripControlChars)(view.body.objective)}\n\n${structure}\n`, stderr };
|
|
59
44
|
}
|
|
60
45
|
return { code: 0, stdout: `${JSON.stringify(publicView(found), null, 2)}\n`, stderr };
|
|
61
46
|
}
|
|
@@ -64,18 +49,10 @@ function registerProcessCommand(program) {
|
|
|
64
49
|
process_
|
|
65
50
|
.command('list')
|
|
66
51
|
.description('list process models declared by the installed registries')
|
|
67
|
-
.action(() => emit(runProcessList((0, discover_1.discoverProcessModels)())));
|
|
52
|
+
.action(() => (0, command_result_1.emit)(runProcessList((0, discover_1.discoverProcessModels)())));
|
|
68
53
|
process_
|
|
69
54
|
.command('show <name>')
|
|
70
55
|
.description('show one process model')
|
|
71
56
|
.option('--json', 'emit the parsed model as JSON')
|
|
72
|
-
.action((name, opts) => emit(runProcessShow((0, discover_1.discoverProcessModels)(), name, opts.json === true)));
|
|
73
|
-
}
|
|
74
|
-
function emit(result) {
|
|
75
|
-
if (result.stderr)
|
|
76
|
-
process.stderr.write(result.stderr);
|
|
77
|
-
if (result.stdout)
|
|
78
|
-
process.stdout.write(result.stdout);
|
|
79
|
-
if (result.code !== 0)
|
|
80
|
-
process.exitCode = result.code;
|
|
57
|
+
.action((name, opts) => (0, command_result_1.emit)(runProcessShow((0, discover_1.discoverProcessModels)(), name, opts.json === true)));
|
|
81
58
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.diagnosticsToStderr = diagnosticsToStderr;
|
|
4
|
+
exports.emit = emit;
|
|
5
|
+
/** Formatea diagnósticos como líneas `warning: ...\n` listas para stderr.
|
|
6
|
+
* Compartido entre comandos para no divergir el formato. */
|
|
7
|
+
function diagnosticsToStderr(diagnostics) {
|
|
8
|
+
return diagnostics.map((d) => `warning: ${d}\n`).join('');
|
|
9
|
+
}
|
|
10
|
+
function emit(result) {
|
|
11
|
+
if (result.stderr)
|
|
12
|
+
process.stderr.write(result.stderr);
|
|
13
|
+
if (result.stdout)
|
|
14
|
+
process.stdout.write(result.stdout);
|
|
15
|
+
if (result.code !== 0)
|
|
16
|
+
process.exitCode = result.code;
|
|
17
|
+
}
|
|
@@ -4,11 +4,13 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.sha256 = sha256;
|
|
7
|
+
exports.composedOrchestrators = composedOrchestrators;
|
|
7
8
|
exports.buildContext = buildContext;
|
|
8
9
|
// cli/src/core/context/provider.ts
|
|
9
10
|
const crypto_1 = __importDefault(require("crypto"));
|
|
10
11
|
const fs_1 = __importDefault(require("fs"));
|
|
11
12
|
const path_1 = __importDefault(require("path"));
|
|
13
|
+
const text_1 = require("../text");
|
|
12
14
|
function sha256(input) {
|
|
13
15
|
return crypto_1.default.createHash('sha256').update(input, 'utf-8').digest('hex');
|
|
14
16
|
}
|
|
@@ -17,29 +19,36 @@ function parseVersion(skill) {
|
|
|
17
19
|
return m ? m[1].trim() : '0.0.0';
|
|
18
20
|
}
|
|
19
21
|
/**
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
22
|
+
* La lista de orquestadores declarados TAL COMO entra al payload de contexto:
|
|
23
|
+
* saneada campo por campo con `sanitizeDeclaredField` (core/text.ts) — que
|
|
24
|
+
* neutraliza tanto marcado markdown/saltos de linea (prompt-injection via
|
|
25
|
+
* `##`, `` ` ``, `*`, `_`, `<`, `>`) COMO bytes de control C0/DEL (ANSI
|
|
26
|
+
* escapes hacia una terminal real). `readDeclaredOrchestrators` solo valida
|
|
27
|
+
* que los campos sean strings no vacios; el saneo pertenece a esta frontera
|
|
28
|
+
* de render, no a la validacion de lectura.
|
|
29
|
+
*
|
|
30
|
+
* ESTA es la UNICA funcion que hace este saneo — es la que consume
|
|
31
|
+
* `renderDeclared` (el payload materializado en cada sesion de agente via
|
|
32
|
+
* `buildContext`) y la que consume `awm context orchestrators` (comando).
|
|
33
|
+
* Ningun consumidor necesita sanear de nuevo por su cuenta: si el comando
|
|
34
|
+
* aplicara su propio saneo, comando y payload podrian divergir en silencio
|
|
35
|
+
* — el modo de falla que R5.2 prohíbe para el modelo de proceso y que vale
|
|
36
|
+
* igual acá. Por la misma razon, cualquier comparacion contra un nombre
|
|
37
|
+
* declarado (p.ej. `--verify` en `commands/context/index.ts`) debe normalizar
|
|
38
|
+
* su lado tambien con `sanitizeDeclaredField`, no con una copia del regex.
|
|
29
39
|
*/
|
|
30
|
-
function
|
|
31
|
-
return
|
|
40
|
+
function composedOrchestrators(list) {
|
|
41
|
+
return list.map(o => ({
|
|
42
|
+
name: (0, text_1.sanitizeDeclaredField)(o.name),
|
|
43
|
+
appliesWhen: (0, text_1.sanitizeDeclaredField)(o.appliesWhen),
|
|
44
|
+
terminatesTo: (0, text_1.sanitizeDeclaredField)(o.terminatesTo),
|
|
45
|
+
}));
|
|
32
46
|
}
|
|
33
47
|
function renderDeclared(list) {
|
|
34
48
|
if (list.length === 0)
|
|
35
49
|
return '';
|
|
36
|
-
const rows = list
|
|
37
|
-
.map(o => {
|
|
38
|
-
const name = sanitizeForMarkdown(o.name);
|
|
39
|
-
const appliesWhen = sanitizeForMarkdown(o.appliesWhen);
|
|
40
|
-
const terminatesTo = sanitizeForMarkdown(o.terminatesTo);
|
|
41
|
-
return `- **${name}** — applies when: ${appliesWhen}. Terminates to: \`${terminatesTo}\`.`;
|
|
42
|
-
})
|
|
50
|
+
const rows = composedOrchestrators(list)
|
|
51
|
+
.map(o => `- **${o.name}** — applies when: ${o.appliesWhen}. Terminates to: \`${o.terminatesTo}\`.`)
|
|
43
52
|
.join('\n');
|
|
44
53
|
return `## Declared orchestrators\n\nConsider these before the built-in pair:\n\n${rows}\n\n`;
|
|
45
54
|
}
|
|
@@ -17,6 +17,7 @@ exports.collectAndWarn = collectAndWarn;
|
|
|
17
17
|
const fs_1 = __importDefault(require("fs"));
|
|
18
18
|
const path_1 = __importDefault(require("path"));
|
|
19
19
|
const registries_1 = require("./registries");
|
|
20
|
+
const text_1 = require("./text");
|
|
20
21
|
const ALLOWED_FIELDS = ['name', 'appliesWhen', 'terminatesTo'];
|
|
21
22
|
// These fields are semantically short (a short identity, a short trigger condition, a
|
|
22
23
|
// short target name) — no legitimate declaration needs more than this. A registry is
|
|
@@ -109,21 +110,41 @@ function readDeclaredOrchestrators(root) {
|
|
|
109
110
|
* de emitir ambas filas al markdown compuesto, gana la primera en el orden de
|
|
110
111
|
* listRegistries() (= orden de registries.json, ver registries.ts) y la duplicada se
|
|
111
112
|
* descarta con un diagnostico — misma degradacion tolerante (reportar, no lanzar) que el
|
|
112
|
-
* resto de este modulo (R1.2, R5.1).
|
|
113
|
+
* resto de este modulo (R1.2, R5.1). `orch.name` es contenido no confiable del registry:
|
|
114
|
+
* se pasa por `stripControlChars` (core/text.ts) antes de interpolarlo en el diagnostico,
|
|
115
|
+
* que termina crudo en stderr real via `command-result.ts`'s `diagnosticsToStderr`/`emit`.
|
|
116
|
+
*
|
|
117
|
+
* Ademas del dedupe por nombre CRUDO, dos orquestadores con nombres crudos distintos
|
|
118
|
+
* (p.ej. `foo_bar` y `foo*bar`) pueden colisionar DESPUES del saneo que aplica
|
|
119
|
+
* `composedOrchestrators` (core/context/provider.ts) — ambos renderizarian como el mismo
|
|
120
|
+
* `foobar` visible, sin explicacion. Como son declaraciones genuinamente distintas, no se
|
|
121
|
+
* descarta ninguna (eso perderia una declaracion real) — se emite un diagnostico
|
|
122
|
+
* accionable en su lugar. `sanitizeDeclaredField` vive en core/text.ts (no en provider.ts)
|
|
123
|
+
* precisamente para que este modulo -- que debe seguir siendo hoja y no puede importar
|
|
124
|
+
* context/provider.ts, el cual ya importa `DeclaredOrchestrator` DESDE este archivo -- pueda
|
|
125
|
+
* calcular el mismo nombre saneado sin duplicar el regex.
|
|
113
126
|
*/
|
|
114
127
|
function collectDeclaredOrchestrators() {
|
|
115
128
|
const declared = [];
|
|
116
129
|
const diagnostics = [];
|
|
117
130
|
const seenNames = new Set();
|
|
131
|
+
const seenSanitizedNames = new Set();
|
|
118
132
|
for (const reg of (0, registries_1.listRegistries)()) {
|
|
119
133
|
const r = readDeclaredOrchestrators(reg.contentRoot);
|
|
120
134
|
for (const orch of r.orchestrators) {
|
|
135
|
+
const file = path_1.default.join(reg.contentRoot, registries_1.REGISTRY_MANIFEST_NAME);
|
|
121
136
|
if (seenNames.has(orch.name)) {
|
|
122
|
-
|
|
123
|
-
diagnostics.push(`${file}: orchestrator "${orch.name}" duplicates one already declared by an earlier registry — shadowed duplicate dropped`);
|
|
137
|
+
diagnostics.push(`${file}: orchestrator "${(0, text_1.stripControlChars)(orch.name)}" duplicates one already declared by an earlier registry — shadowed duplicate dropped`);
|
|
124
138
|
continue;
|
|
125
139
|
}
|
|
126
140
|
seenNames.add(orch.name);
|
|
141
|
+
const sanitizedName = (0, text_1.sanitizeDeclaredField)(orch.name);
|
|
142
|
+
if (seenSanitizedNames.has(sanitizedName)) {
|
|
143
|
+
diagnostics.push(`${file}: orchestrator "${(0, text_1.stripControlChars)(orch.name)}" sanitizes to the same composed name "${sanitizedName}" as an already declared orchestrator — both will render as indistinguishable duplicates in the composed context`);
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
seenSanitizedNames.add(sanitizedName);
|
|
147
|
+
}
|
|
127
148
|
declared.push(orch);
|
|
128
149
|
}
|
|
129
150
|
diagnostics.push(...r.diagnostics);
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.stripControlChars = stripControlChars;
|
|
4
|
+
exports.sanitizeDeclaredField = sanitizeDeclaredField;
|
|
5
|
+
// cli/src/core/text.ts
|
|
6
|
+
/** Neutraliza bytes de control C0 (incluyendo ESC `\x1b`) y DEL de texto que
|
|
7
|
+
* proviene de un registry no confiable antes de escribirlo a una terminal
|
|
8
|
+
* humana. `\n` y `\t` se preservan porque son whitespace legítimo.
|
|
9
|
+
*
|
|
10
|
+
* Vive en core/ y no dentro de un comando porque tiene DOS consumidores:
|
|
11
|
+
* `awm process show` (vista de texto del modelo) y `awm context orchestrators`
|
|
12
|
+
* (campos declarados por un registry). Duplicarlo es la forma exacta que
|
|
13
|
+
* `AGENTS.md` documenta bajo `defensive-guard-consistency`: endurecer una copia
|
|
14
|
+
* y dejar la otra atrás. */
|
|
15
|
+
function stripControlChars(text) {
|
|
16
|
+
// eslint-disable-next-line no-control-regex -- necesitamos matchear C0 deliberadamente
|
|
17
|
+
return text.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '');
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Neutraliza marcado markdown (colapsa `\r?\n` a un espacio y quita
|
|
21
|
+
* `` ` * _ # < > ``) de contenido no confiable antes de interpolarlo en un
|
|
22
|
+
* bloque markdown. Sin esto, un registry malicioso/comprometido podria
|
|
23
|
+
* inyectar saltos de linea, marcadores markdown o pseudo-tags XML/HTML para
|
|
24
|
+
* forjar una seccion nueva o un bloque instruccional dentro del payload de
|
|
25
|
+
* contexto que consume el proveedor de IA — un vector de prompt-injection.
|
|
26
|
+
*/
|
|
27
|
+
function sanitizeMarkdown(s) {
|
|
28
|
+
return s.replace(/\r?\n/g, ' ').replace(/[`*_#<>]/g, '');
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Saneo COMPLETO de un campo de orquestador declarado (name/appliesWhen/
|
|
32
|
+
* terminatesTo) proveniente de un registry no confiable: primero
|
|
33
|
+
* `sanitizeMarkdown` (colapsa saltos de linea, quita marcado markdown), y
|
|
34
|
+
* DESPUES `stripControlChars` (quita bytes de control C0/DEL). Ese orden
|
|
35
|
+
* importa: `sanitizeMarkdown` colapsa `\r?\n` a un espacio antes de que
|
|
36
|
+
* `stripControlChars` pueda tocarlos — aunque `stripControlChars` preserva
|
|
37
|
+
* `\n`/`\t` de todos modos, para cuando corre ya no queda ninguno.
|
|
38
|
+
*
|
|
39
|
+
* Unica fuente de verdad para este saneo: `composedOrchestrators`
|
|
40
|
+
* (core/context/provider.ts) lo aplica a cada campo antes de que llegue al
|
|
41
|
+
* payload de contexto materializado, y cualquier otro punto del CLI que
|
|
42
|
+
* necesite el mismo nombre normalizado (p.ej. `--verify` en
|
|
43
|
+
* `commands/context/index.ts`, o la deteccion de colisiones post-saneo en
|
|
44
|
+
* `core/orchestrators.ts`) debe importar y reusar esta funcion en vez de
|
|
45
|
+
* reimplementar el regex — ver AGENTS.md
|
|
46
|
+
* `reusar-guarda-de-symlink-en-lectores-de-contenido-de-registry` para el
|
|
47
|
+
* patron general de no duplicar guardas sobre contenido de registry.
|
|
48
|
+
*
|
|
49
|
+
* Vive en core/text.ts (hoja pura, cero dependencias) en vez de en
|
|
50
|
+
* provider.ts porque `core/orchestrators.ts` tambien debe consumirla (para
|
|
51
|
+
* detectar colisiones post-saneo entre nombres crudos distintos) y
|
|
52
|
+
* `orchestrators.ts` no puede importar `context/provider.ts`: provider.ts ya
|
|
53
|
+
* importa el tipo `DeclaredOrchestrator` DESDE orchestrators.ts, asi que
|
|
54
|
+
* importar en la direccion contraria cerraria un ciclo real.
|
|
55
|
+
*/
|
|
56
|
+
function sanitizeDeclaredField(s) {
|
|
57
|
+
return stripControlChars(sanitizeMarkdown(s));
|
|
58
|
+
}
|
package/dist/src/index.js
CHANGED
|
@@ -43,6 +43,7 @@ const watch_1 = require("./commands/watch");
|
|
|
43
43
|
const track_1 = require("./commands/track");
|
|
44
44
|
const evidence_1 = require("./commands/evidence");
|
|
45
45
|
const process_1 = require("./commands/process");
|
|
46
|
+
const context_1 = require("./commands/context");
|
|
46
47
|
const add_1 = require("./commands/add");
|
|
47
48
|
const sync_1 = require("./commands/sync");
|
|
48
49
|
const update_1 = require("./commands/update");
|
|
@@ -737,6 +738,7 @@ miroCmd.command('sync <storyMapPath>')
|
|
|
737
738
|
(0, track_1.registerTrackCommand)(program);
|
|
738
739
|
(0, evidence_1.registerEvidenceCommand)(program);
|
|
739
740
|
(0, process_1.registerProcessCommand)(program);
|
|
741
|
+
(0, context_1.registerContextCommand)(program);
|
|
740
742
|
// Commander only waits for async action handlers through parseAsync(). The CLI has
|
|
741
743
|
// async commands (including `sensors coverage`), so returning its promise keeps the
|
|
742
744
|
// process alive until their JSON/output contract has been completed.
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const context_1 = require("../../src/commands/context");
|
|
4
|
+
const collected = (declared, diagnostics = []) => ({ declared, diagnostics });
|
|
5
|
+
const uno = { name: 'mi-proceso', appliesWhen: 'cuando hay una tarea', terminatesTo: 'development-process' };
|
|
6
|
+
describe('awm context orchestrators', () => {
|
|
7
|
+
it('lista los orquestadores compuestos', () => {
|
|
8
|
+
const r = (0, context_1.runContextOrchestrators)(collected([uno]), { json: false });
|
|
9
|
+
expect(r.code).toBe(0);
|
|
10
|
+
expect(r.stdout).toContain('mi-proceso');
|
|
11
|
+
expect(r.stdout).toContain('development-process');
|
|
12
|
+
});
|
|
13
|
+
it('sin declarados sale 0 y lo dice, no falla', () => {
|
|
14
|
+
const r = (0, context_1.runContextOrchestrators)(collected([]), { json: false });
|
|
15
|
+
expect(r.code).toBe(0);
|
|
16
|
+
expect(r.stdout).toMatch(/no declared orchestrators/i);
|
|
17
|
+
});
|
|
18
|
+
it('emite los diagnosticos sin dejar de listar los sanos', () => {
|
|
19
|
+
const r = (0, context_1.runContextOrchestrators)(collected([uno], ['/r/awm-registry.json: boom']), { json: false });
|
|
20
|
+
expect(r.code).toBe(0);
|
|
21
|
+
expect(r.stdout).toContain('mi-proceso');
|
|
22
|
+
expect(r.stderr).toContain('boom');
|
|
23
|
+
});
|
|
24
|
+
it('--json emite la lista compuesta como JSON', () => {
|
|
25
|
+
const r = (0, context_1.runContextOrchestrators)(collected([uno]), { json: true });
|
|
26
|
+
expect(r.code).toBe(0);
|
|
27
|
+
expect(JSON.parse(r.stdout)).toEqual({ orchestrators: [uno] });
|
|
28
|
+
});
|
|
29
|
+
it('--verify sale 0 cuando el nombre esta compuesto', () => {
|
|
30
|
+
const r = (0, context_1.runContextOrchestrators)(collected([uno]), { json: false, verify: 'mi-proceso' });
|
|
31
|
+
expect(r.code).toBe(0);
|
|
32
|
+
expect(r.stdout).toMatch(/composed/i);
|
|
33
|
+
});
|
|
34
|
+
it('--verify sale 2 y nombra lo disponible cuando no esta', () => {
|
|
35
|
+
const r = (0, context_1.runContextOrchestrators)(collected([uno]), { json: false, verify: 'no-existe' });
|
|
36
|
+
expect(r.code).toBe(2);
|
|
37
|
+
expect(r.stderr).toContain('no-existe');
|
|
38
|
+
expect(r.stderr).toContain('mi-proceso');
|
|
39
|
+
});
|
|
40
|
+
it('--verify sale 0 cuando el nombre declarado tiene caracteres que el saneo quita', () => {
|
|
41
|
+
// El nombre REAL declarado por el registry es 'task_capture' (con guion bajo).
|
|
42
|
+
// composed[].name sale saneado ('taskcapture'), pero el usuario tipea el nombre
|
|
43
|
+
// declarado EXACTO via --verify — eso debe seguir contando como compuesto.
|
|
44
|
+
const declarado = { name: 'task_capture', appliesWhen: 'x', terminatesTo: 'y' };
|
|
45
|
+
const r = (0, context_1.runContextOrchestrators)(collected([declarado]), { json: false, verify: 'task_capture' });
|
|
46
|
+
expect(r.code).toBe(0);
|
|
47
|
+
expect(r.stdout).toMatch(/composed/i);
|
|
48
|
+
});
|
|
49
|
+
it('--verify normaliza markdown-especiales del argumento antes de comparar', () => {
|
|
50
|
+
const declarado = { name: 'foo*bar#baz', appliesWhen: 'x', terminatesTo: 'y' };
|
|
51
|
+
const r = (0, context_1.runContextOrchestrators)(collected([declarado]), { json: false, verify: 'foo*bar#baz' });
|
|
52
|
+
expect(r.code).toBe(0);
|
|
53
|
+
});
|
|
54
|
+
it('--verify sigue saliendo 2 para un nombre genuinamente ausente', () => {
|
|
55
|
+
const r = (0, context_1.runContextOrchestrators)(collected([uno]), { json: false, verify: 'de-verdad-no-existe' });
|
|
56
|
+
expect(r.code).toBe(2);
|
|
57
|
+
});
|
|
58
|
+
it('sanea bytes de control antes de escribir a la terminal', () => {
|
|
59
|
+
const hostil = { name: 'x\x1b[31m', appliesWhen: 'w\x07', terminatesTo: 't\x00' };
|
|
60
|
+
const r = (0, context_1.runContextOrchestrators)(collected([hostil]), { json: false });
|
|
61
|
+
// eslint-disable-next-line no-control-regex -- verificamos la ausencia deliberada de C0
|
|
62
|
+
expect(r.stdout).not.toMatch(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -180,3 +180,52 @@ describe('buildContext — declared orchestrators', () => {
|
|
|
180
180
|
// NOTE: the 'generic robustness invariant' test that validated specific prose in the
|
|
181
181
|
// using-awm SKILL.md has been removed — content now lives in awm-baseline-registry
|
|
182
182
|
// (an external repo), not in this monorepo. Content-level tests belong there.
|
|
183
|
+
describe('composedOrchestrators', () => {
|
|
184
|
+
it('devuelve los valores tal como entran al payload, ya saneados', () => {
|
|
185
|
+
const out = (0, provider_1.composedOrchestrators)([
|
|
186
|
+
{ name: 'mi-proceso', appliesWhen: 'cuando *algo*', terminatesTo: 'development-process' },
|
|
187
|
+
]);
|
|
188
|
+
expect(out).toEqual([
|
|
189
|
+
{ name: 'mi-proceso', appliesWhen: 'cuando algo', terminatesTo: 'development-process' },
|
|
190
|
+
]);
|
|
191
|
+
});
|
|
192
|
+
it('neutraliza saltos de linea y markdown estructural', () => {
|
|
193
|
+
const out = (0, provider_1.composedOrchestrators)([
|
|
194
|
+
{ name: 'x', appliesWhen: 'a\n## Forjado', terminatesTo: '`b`' },
|
|
195
|
+
]);
|
|
196
|
+
expect(out[0].appliesWhen).toBe('a Forjado');
|
|
197
|
+
expect(out[0].terminatesTo).toBe('b');
|
|
198
|
+
});
|
|
199
|
+
it('neutraliza bytes de control C0 (p.ej. ESC) ademas del markdown', () => {
|
|
200
|
+
const out = (0, provider_1.composedOrchestrators)([
|
|
201
|
+
{ name: 'a\x1bx', appliesWhen: 'w\x07', terminatesTo: 't\x00' },
|
|
202
|
+
]);
|
|
203
|
+
expect(out[0].name).toBe('ax');
|
|
204
|
+
// eslint-disable-next-line no-control-regex -- verificamos la ausencia deliberada de C0
|
|
205
|
+
const controlCharPattern = /[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/;
|
|
206
|
+
expect(out[0].name).not.toMatch(controlCharPattern);
|
|
207
|
+
expect(out[0].appliesWhen).not.toMatch(controlCharPattern);
|
|
208
|
+
expect(out[0].terminatesTo).not.toMatch(controlCharPattern);
|
|
209
|
+
});
|
|
210
|
+
it('el payload materializado (buildContext) tampoco contiene bytes de control de un orquestador declarado', () => {
|
|
211
|
+
const root = tmpRegistry('---\nname: using-awm\nversion: "1.0.0"\n---\nBODY');
|
|
212
|
+
const ctx = (0, provider_1.buildContext)({
|
|
213
|
+
registryRoot: root,
|
|
214
|
+
profileExtensions: [],
|
|
215
|
+
declaredOrchestrators: [
|
|
216
|
+
{ name: 'evil\x1b[31m', appliesWhen: 'w', terminatesTo: 't' },
|
|
217
|
+
],
|
|
218
|
+
});
|
|
219
|
+
// eslint-disable-next-line no-control-regex -- verificamos la ausencia deliberada de C0
|
|
220
|
+
expect(ctx.markdown).not.toMatch(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/);
|
|
221
|
+
});
|
|
222
|
+
it('lo que renderiza el payload sale de esta misma funcion', () => {
|
|
223
|
+
// Si renderDeclared dejara de consumirla, el comando y el payload
|
|
224
|
+
// podrian divergir en silencio — que es el modo de falla que R5.2 prohibe.
|
|
225
|
+
const declared = [{ name: 'p', appliesWhen: 'w', terminatesTo: 't' }];
|
|
226
|
+
const composed = (0, provider_1.composedOrchestrators)(declared);
|
|
227
|
+
const reg = tmpRegistry('---\nname: using-awm\nversion: "1.0.0"\n---\nBODY');
|
|
228
|
+
const ctx = (0, provider_1.buildContext)({ registryRoot: reg, profileExtensions: [], declaredOrchestrators: declared });
|
|
229
|
+
expect(ctx.markdown).toContain(`- **${composed[0].name}** — applies when: ${composed[0].appliesWhen}.`);
|
|
230
|
+
});
|
|
231
|
+
});
|
|
@@ -217,6 +217,44 @@ describe('collectDeclaredOrchestrators', () => {
|
|
|
217
217
|
expect(diagnostics[0]).toMatch(/shared/);
|
|
218
218
|
expect(diagnostics[0]).toMatch(/duplicate|shadow/i);
|
|
219
219
|
});
|
|
220
|
+
it('sanea el nombre antes de interpolarlo en el diagnostico de duplicado', () => {
|
|
221
|
+
(0, registries_1.writeRegistriesConfig)([
|
|
222
|
+
{ name: 'first', remote: 'unused' },
|
|
223
|
+
{ name: 'second', remote: 'unused' },
|
|
224
|
+
]);
|
|
225
|
+
const nombreHostil = 'shared\x1b[31m';
|
|
226
|
+
writeManifest('first', {
|
|
227
|
+
orchestrator: { name: nombreHostil, appliesWhen: 'primero', terminatesTo: 'a' },
|
|
228
|
+
});
|
|
229
|
+
writeManifest('second', {
|
|
230
|
+
orchestrator: { name: nombreHostil, appliesWhen: 'segundo', terminatesTo: 'b' },
|
|
231
|
+
});
|
|
232
|
+
const { diagnostics } = (0, orchestrators_1.collectDeclaredOrchestrators)();
|
|
233
|
+
expect(diagnostics).toHaveLength(1);
|
|
234
|
+
// eslint-disable-next-line no-control-regex -- verificamos la ausencia deliberada de C0
|
|
235
|
+
expect(diagnostics[0]).not.toMatch(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/);
|
|
236
|
+
expect(diagnostics[0]).toContain('shared[31m');
|
|
237
|
+
});
|
|
238
|
+
it('emite un diagnostico de colision post-saneo entre nombres crudos distintos, sin descartar ninguno', () => {
|
|
239
|
+
(0, registries_1.writeRegistriesConfig)([
|
|
240
|
+
{ name: 'first', remote: 'unused' },
|
|
241
|
+
{ name: 'second', remote: 'unused' },
|
|
242
|
+
]);
|
|
243
|
+
writeManifest('first', {
|
|
244
|
+
orchestrator: { name: 'foo_bar', appliesWhen: 'x', terminatesTo: 'a' },
|
|
245
|
+
});
|
|
246
|
+
writeManifest('second', {
|
|
247
|
+
orchestrator: { name: 'foo*bar', appliesWhen: 'y', terminatesTo: 'b' },
|
|
248
|
+
});
|
|
249
|
+
const { declared, diagnostics } = (0, orchestrators_1.collectDeclaredOrchestrators)();
|
|
250
|
+
// Ambas declaraciones son genuinamente distintas (nombre crudo distinto) — ninguna se descarta.
|
|
251
|
+
expect(declared).toHaveLength(2);
|
|
252
|
+
expect(declared.map((d) => d.name).sort()).toEqual(['foo*bar', 'foo_bar']);
|
|
253
|
+
// Pero se advierte de la colision post-saneo (ambas renderizan como "foobar").
|
|
254
|
+
expect(diagnostics).toHaveLength(1);
|
|
255
|
+
expect(diagnostics[0]).toMatch(/foobar/);
|
|
256
|
+
expect(diagnostics[0]).toMatch(/collis|same composed name/i);
|
|
257
|
+
});
|
|
220
258
|
it('no dedupea orquestadores con nombres distintos, ambos se conservan', () => {
|
|
221
259
|
(0, registries_1.writeRegistriesConfig)([
|
|
222
260
|
{ name: 'first', remote: 'unused' },
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const text_1 = require("../../src/core/text");
|
|
4
|
+
describe('stripControlChars', () => {
|
|
5
|
+
it('elimina ESC y demas bytes de control C0', () => {
|
|
6
|
+
expect((0, text_1.stripControlChars)('a\x1b[31mb\x00c\x07d')).toBe('a[31mbcd');
|
|
7
|
+
});
|
|
8
|
+
it('preserva \\n y \\t, que son whitespace legitimo', () => {
|
|
9
|
+
expect((0, text_1.stripControlChars)('a\nb\tc')).toBe('a\nb\tc');
|
|
10
|
+
});
|
|
11
|
+
it('elimina DEL (0x7F)', () => {
|
|
12
|
+
expect((0, text_1.stripControlChars)('a\x7Fb')).toBe('ab');
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
describe('sanitizeDeclaredField', () => {
|
|
16
|
+
it('quita marcado markdown Y bytes de control en una sola pasada', () => {
|
|
17
|
+
expect((0, text_1.sanitizeDeclaredField)('a\x1bx')).toBe('ax');
|
|
18
|
+
expect((0, text_1.sanitizeDeclaredField)('a\n## Forjado *b* `c` <d>')).toBe('a Forjado b c d');
|
|
19
|
+
});
|
|
20
|
+
it('no deja ESC ni otros C0 sobrevivir aunque el saneo markdown no los toque', () => {
|
|
21
|
+
// eslint-disable-next-line no-control-regex -- verificamos la ausencia deliberada de C0
|
|
22
|
+
expect((0, text_1.sanitizeDeclaredField)('name\x1b[31mred\x07')).not.toMatch(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/);
|
|
23
|
+
});
|
|
24
|
+
it('es idempotente sobre texto ya limpio', () => {
|
|
25
|
+
const clean = 'a plain name';
|
|
26
|
+
expect((0, text_1.sanitizeDeclaredField)(clean)).toBe(clean);
|
|
27
|
+
expect((0, text_1.sanitizeDeclaredField)((0, text_1.sanitizeDeclaredField)(clean))).toBe(clean);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
@@ -0,0 +1,144 @@
|
|
|
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
|
+
const fs_1 = __importDefault(require("fs"));
|
|
7
|
+
const os_1 = __importDefault(require("os"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const child_process_1 = require("child_process");
|
|
10
|
+
const CLI_ROOT = path_1.default.resolve(__dirname, '../..');
|
|
11
|
+
const DIST = path_1.default.join(CLI_ROOT, 'dist', 'src', 'index.js');
|
|
12
|
+
/** Registry de prueba con un orquestador declarado. Nunca toca el ~/.awm real (R7.4). */
|
|
13
|
+
function seedRegistry() {
|
|
14
|
+
const home = fs_1.default.realpathSync(fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-ctx-e2e-')));
|
|
15
|
+
const awmHome = path_1.default.join(home, '.awm');
|
|
16
|
+
const root = path_1.default.join(awmHome, 'registries', 'test-registry');
|
|
17
|
+
fs_1.default.mkdirSync(path_1.default.join(root, 'skills', 'using-awm'), { recursive: true });
|
|
18
|
+
fs_1.default.writeFileSync(path_1.default.join(awmHome, 'registries.json'), JSON.stringify([{ name: 'test-registry', remote: 'https://example.invalid/test.git' }]));
|
|
19
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'awm-registry.json'), JSON.stringify({
|
|
20
|
+
minCliVersion: '8.5.0',
|
|
21
|
+
orchestrator: { name: 'ejemplo-proceso', appliesWhen: 'cuando hay una tarea sin plan', terminatesTo: 'development-process' },
|
|
22
|
+
}));
|
|
23
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'skills', 'using-awm', 'SKILL.md'), '---\nname: using-awm\nversion: "1.0.0"\n---\n\n# using-awm\n');
|
|
24
|
+
return home;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Registry con un nombre de orquestador REALISTA: `task_capture_process`, con guion bajo —
|
|
28
|
+
* la misma clase de caracter (`_`) que disparaba el falso-negativo de `--verify` fijado en
|
|
29
|
+
* 3e8ce65 (arg crudo vs. nombre saneado). `seedRegistry()` usa un nombre puramente
|
|
30
|
+
* alfanumerico-con-guiones que nunca ejercito esa clase de caracter a nivel del binario
|
|
31
|
+
* real — este fixture cierra ese hueco (Finding 1 confirmado del QA post-implementacion) y
|
|
32
|
+
* ademas es representativo de lo que la generacion de `process-lifecycle` (Task 5)
|
|
33
|
+
* plausiblemente produce a partir de un `name` tipo slug.
|
|
34
|
+
*/
|
|
35
|
+
function seedRealisticRegistry() {
|
|
36
|
+
const home = fs_1.default.realpathSync(fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-ctx-e2e-real-')));
|
|
37
|
+
const awmHome = path_1.default.join(home, '.awm');
|
|
38
|
+
const root = path_1.default.join(awmHome, 'registries', 'test-registry');
|
|
39
|
+
fs_1.default.mkdirSync(path_1.default.join(root, 'skills', 'using-awm'), { recursive: true });
|
|
40
|
+
fs_1.default.writeFileSync(path_1.default.join(awmHome, 'registries.json'), JSON.stringify([{ name: 'test-registry', remote: 'https://example.invalid/test.git' }]));
|
|
41
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'awm-registry.json'), JSON.stringify({
|
|
42
|
+
minCliVersion: '8.5.0',
|
|
43
|
+
orchestrator: {
|
|
44
|
+
name: 'task_capture_process',
|
|
45
|
+
appliesWhen: 'cuando llega una tarea sin capturar en el backlog',
|
|
46
|
+
terminatesTo: 'development-process',
|
|
47
|
+
},
|
|
48
|
+
}));
|
|
49
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'skills', 'using-awm', 'SKILL.md'), '---\nname: using-awm\nversion: "1.0.0"\n---\n\n# using-awm\n');
|
|
50
|
+
return home;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* DOS registries en el mismo `AWM_HOME`: uno con declaracion valida ('sana') y otro con una
|
|
54
|
+
* declaracion rota (le falta `terminatesTo`) que `readDeclaredOrchestrators`
|
|
55
|
+
* (core/orchestrators.ts) diagnostica sin lanzar (R1.2) — rechaza SOLO esa declaracion,
|
|
56
|
+
* sin invalidar el registry que la contiene ni a los demas. Ejercita R7.1 a nivel del
|
|
57
|
+
* binario real: diagnosticos por stderr sin dejar de listar lo sano por stdout
|
|
58
|
+
* (Finding 2 confirmado del QA post-implementacion, hasta ahora solo unit-testeado en
|
|
59
|
+
* tests/commands/context.test.ts).
|
|
60
|
+
*/
|
|
61
|
+
function seedMixedRegistries() {
|
|
62
|
+
const home = fs_1.default.realpathSync(fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-ctx-e2e-mixed-')));
|
|
63
|
+
const awmHome = path_1.default.join(home, '.awm');
|
|
64
|
+
const sanoRoot = path_1.default.join(awmHome, 'registries', 'sano-registry');
|
|
65
|
+
const rotoRoot = path_1.default.join(awmHome, 'registries', 'roto-registry');
|
|
66
|
+
fs_1.default.mkdirSync(path_1.default.join(sanoRoot, 'skills', 'using-awm'), { recursive: true });
|
|
67
|
+
fs_1.default.mkdirSync(path_1.default.join(rotoRoot, 'skills', 'using-awm'), { recursive: true });
|
|
68
|
+
fs_1.default.writeFileSync(path_1.default.join(awmHome, 'registries.json'), JSON.stringify([
|
|
69
|
+
{ name: 'sano-registry', remote: 'https://example.invalid/sano.git' },
|
|
70
|
+
{ name: 'roto-registry', remote: 'https://example.invalid/roto.git' },
|
|
71
|
+
]));
|
|
72
|
+
fs_1.default.writeFileSync(path_1.default.join(sanoRoot, 'awm-registry.json'), JSON.stringify({
|
|
73
|
+
minCliVersion: '8.5.0',
|
|
74
|
+
orchestrator: { name: 'proceso-sano', appliesWhen: 'cuando hay una tarea sin plan', terminatesTo: 'development-process' },
|
|
75
|
+
}));
|
|
76
|
+
// Declaracion rota: falta "terminatesTo" -- readDeclaredOrchestrators la rechaza y
|
|
77
|
+
// reporta un diagnostico en vez de lanzar o componerla silenciosamente (R1.2).
|
|
78
|
+
fs_1.default.writeFileSync(path_1.default.join(rotoRoot, 'awm-registry.json'), JSON.stringify({
|
|
79
|
+
minCliVersion: '8.5.0',
|
|
80
|
+
orchestrator: { name: 'proceso-roto', appliesWhen: 'cuando algo pasa' },
|
|
81
|
+
}));
|
|
82
|
+
fs_1.default.writeFileSync(path_1.default.join(sanoRoot, 'skills', 'using-awm', 'SKILL.md'), '---\nname: using-awm\nversion: "1.0.0"\n---\n\n# using-awm\n');
|
|
83
|
+
fs_1.default.writeFileSync(path_1.default.join(rotoRoot, 'skills', 'using-awm', 'SKILL.md'), '---\nname: using-awm\nversion: "1.0.0"\n---\n\n# using-awm\n');
|
|
84
|
+
return home;
|
|
85
|
+
}
|
|
86
|
+
function run(home, ...args) {
|
|
87
|
+
return (0, child_process_1.spawnSync)(process.execPath, [DIST, ...args], {
|
|
88
|
+
cwd: CLI_ROOT, encoding: 'utf8',
|
|
89
|
+
env: { ...process.env, HOME: home, AWM_HOME: path_1.default.join(home, '.awm'), AWM_NO_UPDATE_CHECK: '1' },
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
describe('awm context orchestrators (binario real)', () => {
|
|
93
|
+
let home;
|
|
94
|
+
beforeAll(() => { home = seedRegistry(); });
|
|
95
|
+
afterAll(() => fs_1.default.rmSync(home, { recursive: true, force: true }));
|
|
96
|
+
it('reporta el orquestador declarado por el registry sembrado', () => {
|
|
97
|
+
const r = run(home, 'context', 'orchestrators');
|
|
98
|
+
expect(r.status).toBe(0);
|
|
99
|
+
expect(r.stdout).toContain('ejemplo-proceso');
|
|
100
|
+
});
|
|
101
|
+
it('--json emite la lista parseable', () => {
|
|
102
|
+
const r = run(home, 'context', 'orchestrators', '--json');
|
|
103
|
+
expect(r.status).toBe(0);
|
|
104
|
+
expect(JSON.parse(r.stdout).orchestrators[0].name).toBe('ejemplo-proceso');
|
|
105
|
+
});
|
|
106
|
+
it('--verify cierra el ciclo de verificacion con exit code', () => {
|
|
107
|
+
expect(run(home, 'context', 'orchestrators', '--verify', 'ejemplo-proceso').status).toBe(0);
|
|
108
|
+
const ausente = run(home, 'context', 'orchestrators', '--verify', 'no-existe');
|
|
109
|
+
expect(ausente.status).toBe(2);
|
|
110
|
+
expect(ausente.stderr).toContain('ejemplo-proceso');
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
describe('awm context orchestrators -- nombre realista con guion bajo (binario real)', () => {
|
|
114
|
+
let home;
|
|
115
|
+
beforeAll(() => { home = seedRealisticRegistry(); });
|
|
116
|
+
afterAll(() => fs_1.default.rmSync(home, { recursive: true, force: true }));
|
|
117
|
+
it('--verify con el nombre RAW declarado (con guion bajo) compone -- regression del fix 3e8ce65', () => {
|
|
118
|
+
const r = run(home, 'context', 'orchestrators', '--verify', 'task_capture_process');
|
|
119
|
+
expect(r.status).toBe(0);
|
|
120
|
+
expect(r.stdout).toMatch(/composed/i);
|
|
121
|
+
});
|
|
122
|
+
it('la lista en texto plano y --json muestran el nombre saneado (guion bajo removido)', () => {
|
|
123
|
+
const plano = run(home, 'context', 'orchestrators');
|
|
124
|
+
expect(plano.status).toBe(0);
|
|
125
|
+
expect(plano.stdout).toContain('taskcaptureprocess');
|
|
126
|
+
expect(plano.stdout).not.toContain('task_capture_process');
|
|
127
|
+
const json = run(home, 'context', 'orchestrators', '--json');
|
|
128
|
+
expect(json.status).toBe(0);
|
|
129
|
+
expect(JSON.parse(json.stdout).orchestrators[0].name).toBe('taskcaptureprocess');
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
describe('awm context orchestrators -- diagnosticos + sanos mezclados (binario real)', () => {
|
|
133
|
+
let home;
|
|
134
|
+
beforeAll(() => { home = seedMixedRegistries(); });
|
|
135
|
+
afterAll(() => fs_1.default.rmSync(home, { recursive: true, force: true }));
|
|
136
|
+
it('lista el orquestador sano por stdout y diagnostica el roto por stderr sin fallar', () => {
|
|
137
|
+
const r = run(home, 'context', 'orchestrators');
|
|
138
|
+
expect(r.status).toBe(0);
|
|
139
|
+
expect(r.stdout).toContain('proceso-sano');
|
|
140
|
+
expect(r.stderr).toContain('warning:');
|
|
141
|
+
expect(r.stderr).toContain('roto-registry');
|
|
142
|
+
expect(r.stderr).toContain('terminatesTo');
|
|
143
|
+
});
|
|
144
|
+
});
|