agentic-workflow-manager 9.0.2 → 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/ledger/index.js +13 -0
- package/dist/src/commands/process/index.js +58 -0
- package/dist/src/core/command-result.js +17 -0
- package/dist/src/core/context/provider.js +27 -18
- package/dist/src/core/dashboard/collect.js +30 -5
- package/dist/src/core/dashboard/sanitize.js +24 -2
- package/dist/src/core/orchestrators.js +24 -3
- package/dist/src/core/process/body.js +181 -0
- package/dist/src/core/process/discover.js +94 -0
- package/dist/src/core/process/model.js +109 -0
- package/dist/src/core/process/types.js +5 -0
- package/dist/src/core/text.js +58 -0
- package/dist/src/index.js +4 -0
- package/dist/tests/commands/context.test.js +64 -0
- package/dist/tests/commands/ledger/index.test.js +11 -0
- package/dist/tests/commands/process.test.js +88 -0
- package/dist/tests/core/context/provider.test.js +49 -0
- package/dist/tests/core/dashboard/collect.test.js +25 -21
- package/dist/tests/core/dashboard/processes-section.test.js +71 -0
- package/dist/tests/core/dashboard/production-adapters.test.js +58 -0
- package/dist/tests/core/orchestrators.test.js +38 -0
- package/dist/tests/core/process/body.test.js +239 -0
- package/dist/tests/core/process/discover.test.js +139 -0
- package/dist/tests/core/process/model.test.js +83 -0
- package/dist/tests/core/process/no-regression.test.js +57 -0
- package/dist/tests/core/text.test.js +29 -0
- package/dist/tests/integration/context-orchestrators-e2e.test.js +144 -0
- package/dist/tests/integration/process-model-e2e.test.js +161 -0
- package/dist/tests/structural/process-model-single-parser.test.js +63 -0
- package/package.json +1 -1
|
@@ -0,0 +1,94 @@
|
|
|
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
|
+
exports.discoverProcessModels = discoverProcessModels;
|
|
7
|
+
// cli/src/core/process/discover.ts
|
|
8
|
+
// Enumera modelos de proceso entre todos los registries instalados.
|
|
9
|
+
//
|
|
10
|
+
// Reusa discoverSkills (core/discovery.ts) para listar cada root: no re-camina
|
|
11
|
+
// el filesystem, delega en ella la lectura del directorio `skills/` y la
|
|
12
|
+
// descripción del SKILL.md. Pero se la llama UN ROOT A LA VEZ, no con todos
|
|
13
|
+
// los roots juntos.
|
|
14
|
+
//
|
|
15
|
+
// Motivo: discoverSkills lanza cuando dos roots tienen una skill con el MISMO
|
|
16
|
+
// nombre de carpeta y ningún override declarado — esa es la política correcta
|
|
17
|
+
// para identidad de skill, pero un modelo de proceso se identifica por su
|
|
18
|
+
// `name` de frontmatter, no por el nombre de su carpeta. Si se le pasaran
|
|
19
|
+
// todos los roots juntos, una colisión de nombre-de-carpeta entre dos
|
|
20
|
+
// registries (con o sin relación entre sí) tumbaría el descubrimiento de
|
|
21
|
+
// TODOS los modelos de proceso de TODOS los registries — justo lo que R7.1
|
|
22
|
+
// prohíbe. El dedupe real de modelos ocurre acá abajo, por `name`, con
|
|
23
|
+
// diagnóstico en vez de excepción; ver `collectDeclaredOrchestrators` para el
|
|
24
|
+
// mismo criterio ("gana el primero en orden de roots").
|
|
25
|
+
const fs_1 = __importDefault(require("fs"));
|
|
26
|
+
const path_1 = __importDefault(require("path"));
|
|
27
|
+
const registries_1 = require("../registries");
|
|
28
|
+
const discovery_1 = require("../discovery");
|
|
29
|
+
const model_1 = require("./model");
|
|
30
|
+
const body_1 = require("./body");
|
|
31
|
+
function discoverProcessModels(roots = (0, registries_1.contentRoots)()) {
|
|
32
|
+
const models = [];
|
|
33
|
+
const diagnostics = [];
|
|
34
|
+
const seen = new Set();
|
|
35
|
+
for (const root of roots) {
|
|
36
|
+
let skills;
|
|
37
|
+
try {
|
|
38
|
+
skills = (0, discovery_1.discoverSkills)([root]);
|
|
39
|
+
}
|
|
40
|
+
catch (e) {
|
|
41
|
+
// Con un solo root, discoverSkills no debería lanzar por colisión
|
|
42
|
+
// (no hay con qué colisionar) — pero sí puede lanzar por I/O. Un
|
|
43
|
+
// registry roto no debe impedir descubrir los demás (R7.1).
|
|
44
|
+
diagnostics.push(`${root}: process model discovery unavailable (${e instanceof Error ? e.message : String(e)})`);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
for (const skill of skills) {
|
|
48
|
+
const file = path_1.default.join(skill.path, 'SKILL.md');
|
|
49
|
+
// Same trust boundary as readDeclaredOrchestrators (orchestrators.ts):
|
|
50
|
+
// a registry is untrusted input, and SKILL.md is registry-declared content.
|
|
51
|
+
// assertRegularRegistryFile throws when the file is a symlink (or otherwise
|
|
52
|
+
// not a regular file), which would let a malicious/compromised registry point
|
|
53
|
+
// at an arbitrary local file and have its content read. This module's
|
|
54
|
+
// discipline is "never throws" — a broken/malicious skill must not block
|
|
55
|
+
// discovery of the others — so the rejection is caught here and turned into a
|
|
56
|
+
// diagnostic (naming only the path, never the symlink target's content).
|
|
57
|
+
let exists;
|
|
58
|
+
try {
|
|
59
|
+
exists = (0, registries_1.assertRegularRegistryFile)(file);
|
|
60
|
+
}
|
|
61
|
+
catch (e) {
|
|
62
|
+
diagnostics.push(`${file}: cannot read (${e instanceof Error ? e.message : String(e)})`);
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (!exists) {
|
|
66
|
+
diagnostics.push(`${file}: cannot read (ENOENT: no such file)`);
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
let source;
|
|
70
|
+
try {
|
|
71
|
+
source = fs_1.default.readFileSync(file, 'utf-8');
|
|
72
|
+
}
|
|
73
|
+
catch (e) {
|
|
74
|
+
diagnostics.push(`${file}: cannot read (${e instanceof Error ? e.message : String(e)})`);
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
const front = (0, model_1.parseProcessFrontmatter)(source, file);
|
|
78
|
+
diagnostics.push(...front.diagnostics);
|
|
79
|
+
if (!front.model)
|
|
80
|
+
continue; // no es un modelo, o está roto
|
|
81
|
+
const body = (0, body_1.parseProcessBody)(source, file);
|
|
82
|
+
diagnostics.push(...body.diagnostics);
|
|
83
|
+
if (!body.model)
|
|
84
|
+
continue;
|
|
85
|
+
if (seen.has(front.model.name)) {
|
|
86
|
+
diagnostics.push(`${file}: process "${front.model.name}" duplicates one already declared by an earlier registry — shadowed duplicate dropped`);
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
seen.add(front.model.name);
|
|
90
|
+
models.push({ ...front.model, source: file, body: body.model });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return { models, diagnostics };
|
|
94
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PROCESS_NAME = void 0;
|
|
4
|
+
exports.parseProcessFrontmatter = parseProcessFrontmatter;
|
|
5
|
+
// cli/src/core/process/model.ts
|
|
6
|
+
// Lector del contrato de frontmatter del modelo durable de proceso.
|
|
7
|
+
// Disciplina heredada literal de core/orchestrators.ts: NUNCA lanza. Un modelo
|
|
8
|
+
// malformado se rechaza con diagnóstico y no invalida al registry que lo
|
|
9
|
+
// contiene ni a los demás (R7.1).
|
|
10
|
+
const frontmatter_1 = require("../frontmatter");
|
|
11
|
+
const types_1 = require("./types");
|
|
12
|
+
/** El discriminador es literal: ningún documento se reconoce como modelo por su
|
|
13
|
+
* cuerpo, sus headings ni su nombre de archivo (R1.2). */
|
|
14
|
+
const DISCRIMINATOR = 'process-model';
|
|
15
|
+
const ALLOWED_FIELDS = ['awm', 'schema', 'name', 'status', 'entry_point', 'terminates_to', 'created', 'updated'];
|
|
16
|
+
const STATUSES = ['draft', 'active'];
|
|
17
|
+
/** El slug es lo único del modelo que puede viajar dentro de un id del Dashboard
|
|
18
|
+
* (ver sanitize.ts). Por eso se valida acá y no en la frontera de render: si
|
|
19
|
+
* admitiera rutas, markup o `=`, el id dejaría de ser seguro por construcción. */
|
|
20
|
+
exports.PROCESS_NAME = /^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$/;
|
|
21
|
+
const DATE = /^\d{4}-\d{2}-\d{2}$/;
|
|
22
|
+
/** Lector de pares `clave: valor` de un frontmatter plano. El contrato no admite
|
|
23
|
+
* anidamiento ni block scalars, así que no se arrastra un parser YAML: cualquier
|
|
24
|
+
* línea que no sea `clave: valor` es un rechazo, no una interpretación. */
|
|
25
|
+
function readPairs(block) {
|
|
26
|
+
const pairs = new Map();
|
|
27
|
+
const problems = [];
|
|
28
|
+
for (const raw of block.split(/\r?\n/)) {
|
|
29
|
+
const line = raw.trim();
|
|
30
|
+
if (line === '' || line.startsWith('#'))
|
|
31
|
+
continue;
|
|
32
|
+
const m = /^([A-Za-z_][A-Za-z0-9_]*)\s*:\s*(.*)$/.exec(line);
|
|
33
|
+
if (!m) {
|
|
34
|
+
problems.push(`line is not a "key: value" pair: ${JSON.stringify(line.slice(0, 80))}`);
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (pairs.has(m[1])) {
|
|
38
|
+
problems.push(`duplicate field ${JSON.stringify(m[1])}`);
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
pairs.set(m[1], m[2].trim());
|
|
42
|
+
}
|
|
43
|
+
return { pairs, problems };
|
|
44
|
+
}
|
|
45
|
+
function parseProcessFrontmatter(source, file) {
|
|
46
|
+
let block;
|
|
47
|
+
try {
|
|
48
|
+
block = (0, frontmatter_1.matchFrontmatterBlock)(source);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return { diagnostics: [] };
|
|
52
|
+
}
|
|
53
|
+
if (block === null)
|
|
54
|
+
return { diagnostics: [] };
|
|
55
|
+
const { pairs, problems } = readPairs(block);
|
|
56
|
+
// Sin discriminador NO es un modelo de proceso: no es un error, es otro
|
|
57
|
+
// documento. Devolver diagnóstico acá inundaría de ruido a todo registry
|
|
58
|
+
// (cada SKILL.md normal caería en esta rama).
|
|
59
|
+
if (pairs.get('awm') !== DISCRIMINATOR)
|
|
60
|
+
return { diagnostics: [] };
|
|
61
|
+
for (const key of pairs.keys()) {
|
|
62
|
+
if (!ALLOWED_FIELDS.includes(key)) {
|
|
63
|
+
problems.push(`unknown field ${JSON.stringify(key)} — the contract admits only ${ALLOWED_FIELDS.join(', ')}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const rawSchema = pairs.get('schema');
|
|
67
|
+
const schema = rawSchema !== undefined && /^\d+$/.test(rawSchema) ? Number(rawSchema) : Number.NaN;
|
|
68
|
+
if (!Number.isInteger(schema) || schema < 1) {
|
|
69
|
+
problems.push('"schema" must be a positive integer');
|
|
70
|
+
}
|
|
71
|
+
else if (schema > types_1.KNOWN_PROCESS_SCHEMA) {
|
|
72
|
+
// R1.4: detenerse e informar. Interpretarlo como el contrato anterior es
|
|
73
|
+
// exactamente lo que este branch existe para impedir.
|
|
74
|
+
return { diagnostics: [`${file}: process model declares schema ${schema}, but this CLI understands up to ${types_1.KNOWN_PROCESS_SCHEMA} — install a newer agentic-workflow-manager to read it`] };
|
|
75
|
+
}
|
|
76
|
+
for (const field of ['name', 'status', 'entry_point', 'terminates_to', 'created', 'updated']) {
|
|
77
|
+
const value = pairs.get(field);
|
|
78
|
+
if (value === undefined || value === '')
|
|
79
|
+
problems.push(`"${field}" is required`);
|
|
80
|
+
}
|
|
81
|
+
const name = pairs.get('name') ?? '';
|
|
82
|
+
if (name !== '' && !exports.PROCESS_NAME.test(name))
|
|
83
|
+
problems.push('"name" must be a lowercase slug (a-z, 0-9, hyphen)');
|
|
84
|
+
const status = pairs.get('status') ?? '';
|
|
85
|
+
if (status !== '' && !STATUSES.includes(status))
|
|
86
|
+
problems.push(`"status" must be one of ${STATUSES.join(', ')}`);
|
|
87
|
+
const entryPointRaw = pairs.get('entry_point') ?? '';
|
|
88
|
+
if (entryPointRaw !== '' && entryPointRaw !== 'true' && entryPointRaw !== 'false')
|
|
89
|
+
problems.push('"entry_point" must be true or false');
|
|
90
|
+
const terminatesTo = pairs.get('terminates_to') ?? '';
|
|
91
|
+
if (terminatesTo !== '' && terminatesTo !== 'none' && !exports.PROCESS_NAME.test(terminatesTo)) {
|
|
92
|
+
problems.push('"terminates_to" must be a lowercase slug or "none"');
|
|
93
|
+
}
|
|
94
|
+
for (const field of ['created', 'updated']) {
|
|
95
|
+
const value = pairs.get(field) ?? '';
|
|
96
|
+
if (value !== '' && !DATE.test(value))
|
|
97
|
+
problems.push(`"${field}" must be YYYY-MM-DD`);
|
|
98
|
+
}
|
|
99
|
+
if (problems.length > 0) {
|
|
100
|
+
return { diagnostics: [`${file}: invalid process model — ${problems.join('; ')}`] };
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
model: {
|
|
104
|
+
schema, name, status: status, entryPoint: entryPointRaw === 'true',
|
|
105
|
+
terminatesTo, created: pairs.get('created'), updated: pairs.get('updated'),
|
|
106
|
+
},
|
|
107
|
+
diagnostics: [],
|
|
108
|
+
};
|
|
109
|
+
}
|
|
@@ -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
|
@@ -42,6 +42,8 @@ const job_1 = require("./commands/job");
|
|
|
42
42
|
const watch_1 = require("./commands/watch");
|
|
43
43
|
const track_1 = require("./commands/track");
|
|
44
44
|
const evidence_1 = require("./commands/evidence");
|
|
45
|
+
const process_1 = require("./commands/process");
|
|
46
|
+
const context_1 = require("./commands/context");
|
|
45
47
|
const add_1 = require("./commands/add");
|
|
46
48
|
const sync_1 = require("./commands/sync");
|
|
47
49
|
const update_1 = require("./commands/update");
|
|
@@ -735,6 +737,8 @@ miroCmd.command('sync <storyMapPath>')
|
|
|
735
737
|
(0, watch_1.registerWatchCommand)(program);
|
|
736
738
|
(0, track_1.registerTrackCommand)(program);
|
|
737
739
|
(0, evidence_1.registerEvidenceCommand)(program);
|
|
740
|
+
(0, process_1.registerProcessCommand)(program);
|
|
741
|
+
(0, context_1.registerContextCommand)(program);
|
|
738
742
|
// Commander only waits for async action handlers through parseAsync(). The CLI has
|
|
739
743
|
// async commands (including `sensors coverage`), so returning its promise keeps the
|
|
740
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
|
+
});
|
|
@@ -51,6 +51,17 @@ describe('awm ledger CLI', () => {
|
|
|
51
51
|
.toThrow(/defect-class.*kebab-case/i);
|
|
52
52
|
expect((0, store_1.listEntries)(cwd, 'feat-x')).toEqual([]);
|
|
53
53
|
});
|
|
54
|
+
test.each(['test', 'Test', 'test-check', 'test_x', 'placeholder', 'foo', 'bar', 'todo', 'tbd'])('add rejects a placeholder --desc %p before writing', (desc) => {
|
|
55
|
+
expect(() => run(['add', '--branch', 'feat-x', '--polarity', 'finding', '--class', 'logica',
|
|
56
|
+
'--signature', 'sig-1', '--severity', 'blocker', '--desc', desc], cwd))
|
|
57
|
+
.toThrow(/placeholder.*ledger add --help/i);
|
|
58
|
+
expect((0, store_1.listEntries)(cwd, 'feat-x')).toEqual([]);
|
|
59
|
+
});
|
|
60
|
+
test('add accepts a real description that merely contains the word test', () => {
|
|
61
|
+
run(['add', '--branch', 'feat-x', '--polarity', 'finding', '--class', 'logica',
|
|
62
|
+
'--signature', 'sig-1', '--severity', 'blocker', '--desc', 'unit test for parseConfig throws on empty input'], cwd);
|
|
63
|
+
expect((0, store_1.listEntries)(cwd, 'feat-x')).toHaveLength(1);
|
|
64
|
+
});
|
|
54
65
|
test('list emits the branch entries as JSON', () => {
|
|
55
66
|
run(['add', '--branch', 'feat-x', '--polarity', 'win', '--class', 'proceso',
|
|
56
67
|
'--signature', 'good', '--severity', 'info', '--desc', 'nice'], cwd);
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
// cli/tests/commands/process.test.ts
|
|
4
|
+
const process_1 = require("../../src/commands/process");
|
|
5
|
+
function model(over = {}) {
|
|
6
|
+
return {
|
|
7
|
+
schema: 1, name: 'mi-proceso', status: 'draft', entryPoint: true, terminatesTo: 'none',
|
|
8
|
+
created: '2026-08-23', updated: '2026-08-23', source: '/r/skills/mi-proceso/SKILL.md',
|
|
9
|
+
body: {
|
|
10
|
+
objective: 'G — Objetivo.', appliesWhen: 'Siempre.',
|
|
11
|
+
structure: [{ id: 'SG-1', text: 'Uno', operations: [{ id: 'OP-1.1', text: 'Hacer' }] }],
|
|
12
|
+
routing: [{ when: 'Al empezar', requiredState: '', goesTo: 'OP-1.1', endsAt: 'SG-1' }],
|
|
13
|
+
termination: 'none', unverified: ['Nada.'],
|
|
14
|
+
},
|
|
15
|
+
...over,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
describe('awm process list', () => {
|
|
19
|
+
it('reporta los procesos descubiertos', () => {
|
|
20
|
+
const r = (0, process_1.runProcessList)({ models: [model()], diagnostics: [] });
|
|
21
|
+
expect(r.code).toBe(0);
|
|
22
|
+
expect(r.stdout).toContain('mi-proceso');
|
|
23
|
+
expect(r.stdout).toContain('draft');
|
|
24
|
+
});
|
|
25
|
+
it('sin modelos sale 0 y lo dice, no falla', () => {
|
|
26
|
+
const r = (0, process_1.runProcessList)({ models: [], diagnostics: [] });
|
|
27
|
+
expect(r.code).toBe(0);
|
|
28
|
+
expect(r.stdout).toMatch(/no process models/i);
|
|
29
|
+
});
|
|
30
|
+
it('emite los diagnósticos sin dejar de listar los sanos', () => {
|
|
31
|
+
const r = (0, process_1.runProcessList)({ models: [model()], diagnostics: ['/r/x: invalid process model — boom'] });
|
|
32
|
+
expect(r.code).toBe(0);
|
|
33
|
+
expect(r.stdout).toContain('mi-proceso');
|
|
34
|
+
expect(r.stderr).toContain('boom');
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
describe('awm process show --json', () => {
|
|
38
|
+
it('emite el modelo parseado como JSON', () => {
|
|
39
|
+
const r = (0, process_1.runProcessShow)({ models: [model()], diagnostics: [] }, 'mi-proceso', true);
|
|
40
|
+
expect(r.code).toBe(0);
|
|
41
|
+
const parsed = JSON.parse(r.stdout);
|
|
42
|
+
expect(parsed).toEqual(expect.objectContaining({ name: 'mi-proceso', schema: 1, status: 'draft' }));
|
|
43
|
+
expect(parsed.body.routing).toEqual([{ when: 'Al empezar', requiredState: '', goesTo: 'OP-1.1', endsAt: 'SG-1' }]);
|
|
44
|
+
});
|
|
45
|
+
it('el JSON no filtra el path del filesystem del registry', () => {
|
|
46
|
+
const parsed = JSON.parse((0, process_1.runProcessShow)({ models: [model()], diagnostics: [] }, 'mi-proceso', true).stdout);
|
|
47
|
+
expect(JSON.stringify(parsed)).not.toContain('/r/skills');
|
|
48
|
+
});
|
|
49
|
+
it('un nombre inexistente sale 2 y nombra lo disponible', () => {
|
|
50
|
+
const r = (0, process_1.runProcessShow)({ models: [model()], diagnostics: [] }, 'no-existe', true);
|
|
51
|
+
expect(r.code).toBe(2);
|
|
52
|
+
expect(r.stderr).toMatch(/no-existe/);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
describe('awm process show (texto)', () => {
|
|
56
|
+
it('renderiza name/status/objective/structure en modo texto', () => {
|
|
57
|
+
const r = (0, process_1.runProcessShow)({ models: [model()], diagnostics: [] }, 'mi-proceso', false);
|
|
58
|
+
expect(r.code).toBe(0);
|
|
59
|
+
expect(r.stdout).toContain('mi-proceso (draft)');
|
|
60
|
+
expect(r.stdout).toContain('G — Objetivo.');
|
|
61
|
+
expect(r.stdout).toContain('SG-1 — Uno');
|
|
62
|
+
expect(r.stdout).toContain('OP-1.1 — Hacer');
|
|
63
|
+
});
|
|
64
|
+
it('un nombre inexistente en modo texto sale 2 y nombra lo disponible', () => {
|
|
65
|
+
const r = (0, process_1.runProcessShow)({ models: [model()], diagnostics: [] }, 'no-existe', false);
|
|
66
|
+
expect(r.code).toBe(2);
|
|
67
|
+
expect(r.stderr).toMatch(/no-existe/);
|
|
68
|
+
expect(r.stdout).toBe('');
|
|
69
|
+
});
|
|
70
|
+
it('sanea bytes de control/ANSI del body antes de escribir a stdout', () => {
|
|
71
|
+
const hostile = model({
|
|
72
|
+
body: {
|
|
73
|
+
objective: 'G — Objetivo\x1b[31mmalicioso\x1b[0m.',
|
|
74
|
+
appliesWhen: 'Siempre.',
|
|
75
|
+
structure: [{ id: 'SG-1', text: 'Uno\x1b]0;pwned\x07', operations: [{ id: 'OP-1.1', text: 'Hacer\x1b[2Jcosas' }] }],
|
|
76
|
+
routing: [{ when: 'Al empezar', requiredState: '', goesTo: 'OP-1.1', endsAt: 'SG-1' }],
|
|
77
|
+
termination: 'none', unverified: ['Nada.'],
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
const r = (0, process_1.runProcessShow)({ models: [hostile], diagnostics: [] }, 'mi-proceso', false);
|
|
81
|
+
expect(r.code).toBe(0);
|
|
82
|
+
// eslint-disable-next-line no-control-regex -- necesitamos verificar la ausencia deliberada de C0
|
|
83
|
+
expect(r.stdout).not.toMatch(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/);
|
|
84
|
+
expect(r.stdout).toContain('Objetivo[31mmalicioso[0m.');
|
|
85
|
+
expect(r.stdout).toContain('Uno]0;pwned');
|
|
86
|
+
expect(r.stdout).toContain('Hacer[2Jcosas');
|
|
87
|
+
});
|
|
88
|
+
});
|
|
@@ -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
|
+
});
|