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.
@@ -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
- * Neutraliza contenido no confiable proveniente de registries declarados
21
- * (name/appliesWhen/terminatesTo) antes de interpolarlo en markdown.
22
- * Sin esto, un registry malicioso/comprometido podria inyectar saltos de
23
- * linea, marcadores markdown (##, `, *, _) o pseudo-tags XML/HTML (<, >)
24
- * para forjar una seccion nueva o un bloque instruccional dentro del
25
- * payload de contexto que consume el proveedor de IA — un vector de
26
- * prompt-injection. `readDeclaredOrchestrators` solo valida que los
27
- * campos sean strings no vacios; el saneo pertenece a esta frontera de
28
- * render, no a la validacion de lectura.
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 sanitizeForMarkdown(s) {
31
- return s.replace(/\r?\n/g, ' ').replace(/[`*_#<>]/g, '');
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
- const file = path_1.default.join(reg.contentRoot, registries_1.REGISTRY_MANIFEST_NAME);
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
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentic-workflow-manager",
3
- "version": "9.1.0",
3
+ "version": "9.2.0",
4
4
  "main": "dist/src/index.js",
5
5
  "bin": {
6
6
  "awm": "./dist/src/index.js"