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.
Files changed (31) hide show
  1. package/dist/src/commands/context/index.js +47 -0
  2. package/dist/src/commands/ledger/index.js +13 -0
  3. package/dist/src/commands/process/index.js +58 -0
  4. package/dist/src/core/command-result.js +17 -0
  5. package/dist/src/core/context/provider.js +27 -18
  6. package/dist/src/core/dashboard/collect.js +30 -5
  7. package/dist/src/core/dashboard/sanitize.js +24 -2
  8. package/dist/src/core/orchestrators.js +24 -3
  9. package/dist/src/core/process/body.js +181 -0
  10. package/dist/src/core/process/discover.js +94 -0
  11. package/dist/src/core/process/model.js +109 -0
  12. package/dist/src/core/process/types.js +5 -0
  13. package/dist/src/core/text.js +58 -0
  14. package/dist/src/index.js +4 -0
  15. package/dist/tests/commands/context.test.js +64 -0
  16. package/dist/tests/commands/ledger/index.test.js +11 -0
  17. package/dist/tests/commands/process.test.js +88 -0
  18. package/dist/tests/core/context/provider.test.js +49 -0
  19. package/dist/tests/core/dashboard/collect.test.js +25 -21
  20. package/dist/tests/core/dashboard/processes-section.test.js +71 -0
  21. package/dist/tests/core/dashboard/production-adapters.test.js +58 -0
  22. package/dist/tests/core/orchestrators.test.js +38 -0
  23. package/dist/tests/core/process/body.test.js +239 -0
  24. package/dist/tests/core/process/discover.test.js +139 -0
  25. package/dist/tests/core/process/model.test.js +83 -0
  26. package/dist/tests/core/process/no-regression.test.js +57 -0
  27. package/dist/tests/core/text.test.js +29 -0
  28. package/dist/tests/integration/context-orchestrators-e2e.test.js +144 -0
  29. package/dist/tests/integration/process-model-e2e.test.js +161 -0
  30. package/dist/tests/structural/process-model-single-parser.test.js +63 -0
  31. package/package.json +1 -1
@@ -0,0 +1,139 @@
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
+ // cli/tests/core/process/discover.test.ts
7
+ const discover_1 = require("../../../src/core/process/discover");
8
+ const fs_1 = __importDefault(require("fs"));
9
+ const os_1 = __importDefault(require("os"));
10
+ const path_1 = __importDefault(require("path"));
11
+ const FIXTURE = `---
12
+ awm: process-model
13
+ schema: 1
14
+ name: NAME
15
+ status: draft
16
+ entry_point: true
17
+ terminates_to: none
18
+ created: 2026-08-23
19
+ updated: 2026-08-23
20
+ ---
21
+
22
+ ## Objetivo
23
+
24
+ G — Objetivo.
25
+
26
+ ## Cuándo aplica
27
+
28
+ Siempre.
29
+
30
+ ## Estructura
31
+
32
+ - SG-1 — Uno
33
+ - OP-1.1 — Hacer
34
+
35
+ ## Ruteo
36
+
37
+ | Cuándo | Estado requerido | Va a | Termina en |
38
+ |---|---|---|---|
39
+ | Al empezar | | OP-1.1 | SG-1 |
40
+
41
+ ## Terminación
42
+
43
+ none
44
+
45
+ ## Sin verificar
46
+
47
+ - Nada.
48
+ `;
49
+ function registry(root, skills) {
50
+ for (const [name, content] of Object.entries(skills)) {
51
+ fs_1.default.mkdirSync(path_1.default.join(root, 'skills', name), { recursive: true });
52
+ fs_1.default.writeFileSync(path_1.default.join(root, 'skills', name, 'SKILL.md'), content);
53
+ }
54
+ return root;
55
+ }
56
+ describe('discoverProcessModels', () => {
57
+ let tmp;
58
+ beforeEach(() => { tmp = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-process-discover-')); });
59
+ afterEach(() => { fs_1.default.rmSync(tmp, { recursive: true, force: true }); });
60
+ it('encuentra solo los SKILL.md con el discriminador', () => {
61
+ const root = registry(path_1.default.join(tmp, 'r1'), {
62
+ 'mi-proceso': FIXTURE.replace('NAME', 'mi-proceso'),
63
+ 'skill-normal': '---\nname: skill-normal\ndescription: otra cosa\n---\n\n# Normal\n',
64
+ });
65
+ const r = (0, discover_1.discoverProcessModels)([root]);
66
+ expect(r.models.map((m) => m.name)).toEqual(['mi-proceso']);
67
+ expect(r.diagnostics).toEqual([]);
68
+ });
69
+ it('un modelo roto no impide descubrir los sanos', () => {
70
+ const root = registry(path_1.default.join(tmp, 'r2'), {
71
+ 'sano': FIXTURE.replace('NAME', 'sano'),
72
+ 'roto': FIXTURE.replace('NAME', 'roto').replace('schema: 1', 'schema: cero'),
73
+ });
74
+ const r = (0, discover_1.discoverProcessModels)([root]);
75
+ expect(r.models.map((m) => m.name)).toEqual(['sano']);
76
+ expect(r.diagnostics).toHaveLength(1);
77
+ });
78
+ it('deduplica por name entre registries y lo reporta', () => {
79
+ const a = registry(path_1.default.join(tmp, 'a'), { 'dup': FIXTURE.replace('NAME', 'dup') });
80
+ const b = registry(path_1.default.join(tmp, 'b'), { 'dup': FIXTURE.replace('NAME', 'dup') });
81
+ const r = (0, discover_1.discoverProcessModels)([a, b]);
82
+ expect(r.models).toHaveLength(1);
83
+ expect(r.diagnostics.join(' ')).toMatch(/duplicates/i);
84
+ });
85
+ it('sin registries devuelve vacío sin diagnósticos', () => {
86
+ expect((0, discover_1.discoverProcessModels)([])).toEqual({ models: [], diagnostics: [] });
87
+ });
88
+ it('nunca lanza con un root inexistente', () => {
89
+ expect(() => (0, discover_1.discoverProcessModels)([path_1.default.join(tmp, 'no-existe')])).not.toThrow();
90
+ });
91
+ it('un `skills` roto (archivo en vez de directorio) no impide seguir, y queda diagnosticado', () => {
92
+ const root = path_1.default.join(tmp, 'r3');
93
+ fs_1.default.mkdirSync(root, { recursive: true });
94
+ fs_1.default.writeFileSync(path_1.default.join(root, 'skills'), 'no soy un directorio');
95
+ let r;
96
+ expect(() => { r = (0, discover_1.discoverProcessModels)([root]); }).not.toThrow();
97
+ expect(r.models).toEqual([]);
98
+ expect(r.diagnostics).toHaveLength(1);
99
+ expect(r.diagnostics[0]).toContain(root);
100
+ });
101
+ it('un SKILL.md roto (directorio en vez de archivo) no impide seguir, y queda diagnosticado', () => {
102
+ const root = path_1.default.join(tmp, 'r4');
103
+ const skillMd = path_1.default.join(root, 'skills', 'roto', 'SKILL.md');
104
+ fs_1.default.mkdirSync(skillMd, { recursive: true });
105
+ let r;
106
+ expect(() => { r = (0, discover_1.discoverProcessModels)([root]); }).not.toThrow();
107
+ expect(r.models).toEqual([]);
108
+ expect(r.diagnostics).toHaveLength(1);
109
+ expect(r.diagnostics[0]).toContain(skillMd);
110
+ expect(r.diagnostics[0]).toMatch(/cannot read/);
111
+ });
112
+ it('un SKILL.md simbolico apuntando fuera del registry no se lee: ni modelo ni contenido filtrado en diagnósticos', () => {
113
+ const root = path_1.default.join(tmp, 'r5');
114
+ const skillDir = path_1.default.join(root, 'skills', 'malicioso');
115
+ fs_1.default.mkdirSync(skillDir, { recursive: true });
116
+ // Archivo "sensible" FUERA de la raíz del registry — simula ~/.ssh/id_rsa u otro
117
+ // archivo local legible por el usuario que ejecuta `awm`.
118
+ const secretDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-outside-registry-'));
119
+ const secretFile = path_1.default.join(secretDir, 'id_rsa');
120
+ const SECRET_MARKER = 'SUPER-SECRET-PRIVATE-KEY-CONTENT-DO-NOT-LEAK';
121
+ fs_1.default.writeFileSync(secretFile, `-----BEGIN OPENSSH PRIVATE KEY-----\n${SECRET_MARKER}\n-----END OPENSSH PRIVATE KEY-----\n`);
122
+ const skillMd = path_1.default.join(skillDir, 'SKILL.md');
123
+ fs_1.default.symlinkSync(secretFile, skillMd);
124
+ let r;
125
+ expect(() => { r = (0, discover_1.discoverProcessModels)([root]); }).not.toThrow();
126
+ // Ningún modelo se construye a partir del symlink.
127
+ expect(r.models).toEqual([]);
128
+ // Se reporta el problema, pero SOLO nombrando el path — nunca el contenido del
129
+ // archivo apuntado. El nombre del archivo secreto en sí (id_rsa) sirve como proxy
130
+ // de "esto no debería aparecer en diagnósticos" tanto como el contenido.
131
+ expect(r.diagnostics).toHaveLength(1);
132
+ expect(r.diagnostics[0]).toContain(skillMd);
133
+ expect(r.diagnostics[0]).toMatch(/symbolic link/i);
134
+ const allDiagnosticsText = r.diagnostics.join('\n');
135
+ expect(allDiagnosticsText).not.toContain(SECRET_MARKER);
136
+ expect(allDiagnosticsText).not.toContain('BEGIN OPENSSH PRIVATE KEY');
137
+ fs_1.default.rmSync(secretDir, { recursive: true, force: true });
138
+ });
139
+ });
@@ -0,0 +1,83 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const model_1 = require("../../../src/core/process/model");
4
+ const valid = `---
5
+ awm: process-model
6
+ schema: 1
7
+ name: ejemplo-proceso
8
+ status: draft
9
+ entry_point: true
10
+ terminates_to: none
11
+ created: 2026-08-23
12
+ updated: 2026-08-23
13
+ ---
14
+
15
+ # Ejemplo
16
+ `;
17
+ describe('parseProcessFrontmatter', () => {
18
+ it('acepta el modelo canónico', () => {
19
+ const r = (0, model_1.parseProcessFrontmatter)(valid, 'skills/x/SKILL.md');
20
+ expect(r.diagnostics).toEqual([]);
21
+ expect(r.model).toEqual(expect.objectContaining({
22
+ schema: 1, name: 'ejemplo-proceso', status: 'draft', entryPoint: true, terminatesTo: 'none',
23
+ }));
24
+ });
25
+ it('no infiere: sin el discriminador no es un modelo', () => {
26
+ const r = (0, model_1.parseProcessFrontmatter)(valid.replace('awm: process-model\n', ''), 'skills/x/SKILL.md');
27
+ expect(r.model).toBeUndefined();
28
+ expect(r.diagnostics).toEqual([]); // no es un error: es "no es un modelo"
29
+ });
30
+ it('se detiene ante un schema más nuevo en vez de leerlo como el anterior', () => {
31
+ const r = (0, model_1.parseProcessFrontmatter)(valid.replace('schema: 1', 'schema: 2'), 'skills/x/SKILL.md');
32
+ expect(r.model).toBeUndefined();
33
+ expect(r.diagnostics.join(' ')).toMatch(/newer/i);
34
+ });
35
+ it.each(['0', '-1', '1.5', 'uno'])('rechaza schema no-entero-positivo %s', (bad) => {
36
+ const r = (0, model_1.parseProcessFrontmatter)(valid.replace('schema: 1', `schema: ${bad}`), 'skills/x/SKILL.md');
37
+ expect(r.model).toBeUndefined();
38
+ expect(r.diagnostics).not.toEqual([]);
39
+ });
40
+ it.each(['active', 'draft'])('admite status %s', (s) => {
41
+ expect((0, model_1.parseProcessFrontmatter)(valid.replace('status: draft', `status: ${s}`), 'p').model?.status).toBe(s);
42
+ });
43
+ it('rechaza un status inventado', () => {
44
+ const r = (0, model_1.parseProcessFrontmatter)(valid.replace('status: draft', 'status: published'), 'p');
45
+ expect(r.model).toBeUndefined();
46
+ expect(r.diagnostics.join(' ')).toMatch(/status/);
47
+ });
48
+ it('rechaza un campo desconocido en vez de ignorarlo', () => {
49
+ const r = (0, model_1.parseProcessFrontmatter)(valid.replace('created:', 'api_token: ghp_x\ncreated:'), 'p');
50
+ expect(r.model).toBeUndefined();
51
+ expect(r.diagnostics.join(' ')).toMatch(/unknown field/i);
52
+ });
53
+ it('rechaza un name que no es slug', () => {
54
+ const r = (0, model_1.parseProcessFrontmatter)(valid.replace('name: ejemplo-proceso', 'name: ../../etc/passwd'), 'p');
55
+ expect(r.model).toBeUndefined();
56
+ });
57
+ it('nunca lanza ante entrada basura', () => {
58
+ for (const junk of ['', '---\n', '---\n\x00\n---\n', 'no frontmatter']) {
59
+ expect(() => (0, model_1.parseProcessFrontmatter)(junk, 'p')).not.toThrow();
60
+ }
61
+ });
62
+ it.each(['si', 'True', '1', 'yes'])('rechaza un entry_point que no es true/false: %s', (bad) => {
63
+ const r = (0, model_1.parseProcessFrontmatter)(valid.replace('entry_point: true', `entry_point: ${bad}`), 'p');
64
+ expect(r.model).toBeUndefined();
65
+ expect(r.diagnostics.join(' ')).toMatch(/"entry_point" must be true or false/);
66
+ });
67
+ it.each(['../../etc/passwd', 'None', 'Foo-Bar', 'has spaces'])('rechaza un terminates_to que no es slug ni "none": %s', (bad) => {
68
+ const r = (0, model_1.parseProcessFrontmatter)(valid.replace('terminates_to: none', `terminates_to: ${bad}`), 'p');
69
+ expect(r.model).toBeUndefined();
70
+ expect(r.diagnostics.join(' ')).toMatch(/"terminates_to" must be a lowercase slug or "none"/);
71
+ });
72
+ it.each(['created', 'updated'])('rechaza un %s con formato de fecha inválido', (field) => {
73
+ const r = (0, model_1.parseProcessFrontmatter)(valid.replace(`${field}: 2026-08-23`, `${field}: 23-08-2026`), 'p');
74
+ expect(r.model).toBeUndefined();
75
+ expect(r.diagnostics.join(' ')).toMatch(new RegExp(`${field}.*YYYY-MM-DD`));
76
+ });
77
+ it.each(['name', 'status', 'entry_point', 'terminates_to', 'created', 'updated'])('rechaza si falta el campo requerido %s', (field) => {
78
+ const withoutField = valid.split('\n').filter((line) => !line.startsWith(`${field}:`)).join('\n');
79
+ const r = (0, model_1.parseProcessFrontmatter)(withoutField, 'p');
80
+ expect(r.model).toBeUndefined();
81
+ expect(r.diagnostics.join(' ')).toMatch(new RegExp(`"${field}" is required`));
82
+ });
83
+ });
@@ -0,0 +1,57 @@
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
+ // cli/tests/core/process/no-regression.test.ts
7
+ const collect_1 = require("../../../src/core/dashboard/collect");
8
+ const discover_1 = require("../../../src/core/process/discover");
9
+ const fs_1 = __importDefault(require("fs"));
10
+ const os_1 = __importDefault(require("os"));
11
+ const path_1 = __importDefault(require("path"));
12
+ describe('R7 — ausencia de modelos y aislamiento', () => {
13
+ let root;
14
+ let home;
15
+ const realHome = process.env.HOME;
16
+ const realAwmHome = process.env.AWM_HOME;
17
+ beforeEach(() => {
18
+ root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-process-noreg-'));
19
+ home = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-process-home-'));
20
+ fs_1.default.writeFileSync(path_1.default.join(root, 'package.json'), '{}');
21
+ // R7.4: ningún test toca el ~/.awm real.
22
+ process.env.HOME = home;
23
+ process.env.AWM_HOME = path_1.default.join(home, '.awm');
24
+ });
25
+ afterEach(() => {
26
+ if (realHome === undefined)
27
+ delete process.env.HOME;
28
+ else
29
+ process.env.HOME = realHome;
30
+ if (realAwmHome === undefined)
31
+ delete process.env.AWM_HOME;
32
+ else
33
+ process.env.AWM_HOME = realAwmHome;
34
+ fs_1.default.rmSync(root, { recursive: true, force: true });
35
+ fs_1.default.rmSync(home, { recursive: true, force: true });
36
+ });
37
+ it('sin modelos, el snapshot conserva las nueve secciones en orden', () => {
38
+ const snapshot = (0, collect_1.collectDashboardSnapshot)({ cwd: root, now: '2026-08-23T00:00:00.000Z' });
39
+ expect(snapshot.sections.map((s) => s.id)).toEqual(['machine', 'project', 'planning', 'execution', 'qa', 'docs', 'retro', 'history', 'processes']);
40
+ expect(snapshot.schema).toBe(2);
41
+ expect(snapshot.sections.find((s) => s.id === 'processes')).toEqual(expect.objectContaining({ availability: 'not_applicable', items: [] }));
42
+ });
43
+ it('el descubridor no toca el ~/.awm real', () => {
44
+ // contentRoots() (usado por default por discoverProcessModels) lee
45
+ // AWM_HOME dinámicamente en cada llamada — con AWM_HOME apuntando al
46
+ // tmpdir de este test, sin modelos de proceso instalados ahí, esto
47
+ // no debe lanzar. (Se descartó una aserción sobre `.awm/process-cache`:
48
+ // ningún código de este repo escribe ese path, así que nunca podía
49
+ // fallar y no aportaba evidencia real de aislamiento.)
50
+ expect(() => (0, discover_1.discoverProcessModels)()).not.toThrow();
51
+ });
52
+ it('usa path.join y no separadores hardcodeados', () => {
53
+ const src = fs_1.default.readFileSync(path_1.default.resolve(__dirname, '../../../src/core/process/discover.ts'), 'utf-8');
54
+ expect(src).not.toMatch(/['"`][^'"`]*\/skills\//);
55
+ expect(src).toContain('path.join');
56
+ });
57
+ });
@@ -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
+ });
@@ -0,0 +1,161 @@
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
+ // cli/tests/integration/process-model-e2e.test.ts
7
+ //
8
+ // Ledger signature: ca1-2-1-3-no-e2e-regression-test.
9
+ //
10
+ // CA-1.2 y CA-1.3 del plan R1a (docs/plans/2026-08-23-process-model-r1a-plan.md)
11
+ // fueron verificados MANUALMENTE varias veces durante esa sesión, pero ningún
12
+ // test permanente los sostenía. Este test cierra ese hueco ejecutando el
13
+ // binario compilado real (dist/src/index.js) contra un AWM_HOME/registry
14
+ // fixture aislado — no llama funciones internas directamente. El plan es
15
+ // explícito: "No vale el test unitario: el criterio dice 'reporta', y eso se
16
+ // prueba ejecutando" (CA-1.2). Sigue el patrón de invocación de
17
+ // tests/structural/r3-cli-major-version.test.ts (spawnSync del dist compilado)
18
+ // y el patrón de fixture de tests/core/process/discover.test.ts +
19
+ // tests/core/process/no-regression.test.ts (registries.json + skills/<name>/SKILL.md
20
+ // bajo un AWM_HOME de tmpdir, nunca el ~/.awm real).
21
+ const fs_1 = __importDefault(require("fs"));
22
+ const os_1 = __importDefault(require("os"));
23
+ const path_1 = __importDefault(require("path"));
24
+ const child_process_1 = require("child_process");
25
+ const CLI_ROOT = path_1.default.resolve(__dirname, '../..');
26
+ const DIST_ENTRYPOINT = path_1.default.join(CLI_ROOT, 'dist', 'src', 'index.js');
27
+ // Fixture canónico del plan R1a, reproducido byte a byte (bloque "Canonical fixture").
28
+ const EJEMPLO_PROCESO_SKILL_MD = `---
29
+ awm: process-model
30
+ schema: 1
31
+ name: ejemplo-proceso
32
+ status: draft
33
+ entry_point: true
34
+ terminates_to: none
35
+ created: 2026-08-23
36
+ updated: 2026-08-23
37
+ ---
38
+
39
+ # Ejemplo
40
+
41
+ ## Objetivo
42
+
43
+ G — Llevar una idea hasta una rama cerrada.
44
+
45
+ ## Cuándo aplica
46
+
47
+ Cuando hay una tarea de desarrollo sin plan previo.
48
+
49
+ ## Estructura
50
+
51
+ - SG-1 — Diseñar
52
+ - OP-1.1 — Elicitar requisitos
53
+ - OP-1.2 — Escribir el design doc
54
+ - SG-2 — Ejecutar
55
+ - OP-2.1 — Implementar por tasks
56
+
57
+ ## Ruteo
58
+
59
+ | Cuándo | Estado requerido | Va a | Termina en |
60
+ |---|---|---|---|
61
+ | No hay design doc | | OP-1.1 | SG-1 |
62
+ | Design doc aprobado | SG-1 | OP-2.1 | SG-2 |
63
+
64
+ ## Terminación
65
+
66
+ finishing-a-development-branch
67
+
68
+ ## Sin verificar
69
+
70
+ Nada.
71
+ `;
72
+ /** Construye AWM_HOME/registries.json + AWM_HOME/registries/<name>/skills/<skill>/SKILL.md
73
+ * — el mismo layout que contentRoots() (src/core/registries.ts) resuelve para
74
+ * descubrimiento real, sin clonar git: contentRoots() solo necesita que el
75
+ * directorio exista y pase inspectRegistrySafety (ningún symlink, ≥1 dir
76
+ * administrado). El campo "remote" no se usa en este test — nada corre `awm update`. */
77
+ function fixture() {
78
+ const root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-process-e2e-'));
79
+ const project = path_1.default.join(root, 'project');
80
+ const home = path_1.default.join(root, 'home');
81
+ const awmHome = path_1.default.join(home, '.awm');
82
+ const registryRoot = path_1.default.join(awmHome, 'registries', 'baseline');
83
+ fs_1.default.mkdirSync(project, { recursive: true });
84
+ fs_1.default.mkdirSync(path_1.default.join(registryRoot, 'skills', 'ejemplo-proceso'), { recursive: true });
85
+ fs_1.default.writeFileSync(path_1.default.join(project, 'package.json'), JSON.stringify({ name: 'process-model-e2e-fixture', private: true }));
86
+ fs_1.default.writeFileSync(path_1.default.join(awmHome, 'registries.json'), JSON.stringify([{ name: 'baseline', remote: 'https://example.invalid/baseline.git' }]));
87
+ fs_1.default.writeFileSync(path_1.default.join(registryRoot, 'skills', 'ejemplo-proceso', 'SKILL.md'), EJEMPLO_PROCESO_SKILL_MD);
88
+ return { root, project, home, awmHome };
89
+ }
90
+ function runCompiledCli(f, ...args) {
91
+ const result = (0, child_process_1.spawnSync)(process.execPath, [DIST_ENTRYPOINT, ...args], {
92
+ cwd: f.project,
93
+ encoding: 'utf8',
94
+ env: {
95
+ ...process.env,
96
+ HOME: f.home,
97
+ USERPROFILE: f.home,
98
+ AWM_HOME: f.awmHome,
99
+ AWM_NO_UPDATE_CHECK: '1',
100
+ },
101
+ });
102
+ expect(result.error).toBeUndefined();
103
+ return { code: result.status, stdout: result.stdout, stderr: result.stderr };
104
+ }
105
+ describe('process model E2E against the real built binary (CA-1.2, CA-1.3)', () => {
106
+ jest.setTimeout(60_000);
107
+ beforeAll(() => {
108
+ if (!fs_1.default.existsSync(DIST_ENTRYPOINT)) {
109
+ throw new Error(`${DIST_ENTRYPOINT} no existe — este test ejecuta el binario compilado real. ` +
110
+ 'Correr `npm run build` antes de `jest` (mismo pre-requisito que tests/structural/r3-cli-major-version.test.ts).');
111
+ }
112
+ });
113
+ let f;
114
+ beforeEach(() => { f = fixture(); });
115
+ afterEach(() => { fs_1.default.rmSync(f.root, { recursive: true, force: true }); });
116
+ test('CA-1.2 — `process list` reporta ejemplo-proceso con su status', () => {
117
+ const result = runCompiledCli(f, 'process', 'list');
118
+ expect(result.code).toBe(0);
119
+ expect(result.stdout).toContain('ejemplo-proceso');
120
+ expect(result.stdout).toContain('draft');
121
+ });
122
+ test('CA-1.2 — `process show ejemplo-proceso --json` emite el modelo parseado completo', () => {
123
+ const result = runCompiledCli(f, 'process', 'show', 'ejemplo-proceso', '--json');
124
+ expect(result.code).toBe(0);
125
+ const model = JSON.parse(result.stdout);
126
+ expect(model).toEqual(expect.objectContaining({
127
+ name: 'ejemplo-proceso',
128
+ schema: 1,
129
+ status: 'draft',
130
+ entryPoint: true,
131
+ terminatesTo: 'none',
132
+ }));
133
+ expect(model.body.structure).toEqual([
134
+ { id: 'SG-1', text: 'Diseñar', operations: [
135
+ { id: 'OP-1.1', text: 'Elicitar requisitos' },
136
+ { id: 'OP-1.2', text: 'Escribir el design doc' },
137
+ ] },
138
+ { id: 'SG-2', text: 'Ejecutar', operations: [
139
+ { id: 'OP-2.1', text: 'Implementar por tasks' },
140
+ ] },
141
+ ]);
142
+ expect(model.body.routing).toEqual([
143
+ { when: 'No hay design doc', requiredState: '', goesTo: 'OP-1.1', endsAt: 'SG-1' },
144
+ { when: 'Design doc aprobado', requiredState: 'SG-1', goesTo: 'OP-2.1', endsAt: 'SG-2' },
145
+ ]);
146
+ expect(model.body.termination).toBe('finishing-a-development-branch');
147
+ // No source path leaks into the published/consumable view (see publicView
148
+ // in src/commands/process/index.ts) — assert the field is entirely absent.
149
+ expect(model).not.toHaveProperty('source');
150
+ });
151
+ test('CA-1.3 — `doctor --full` renderiza la seccion Processes desde el adapter, sin re-parseo ad-hoc', () => {
152
+ const result = runCompiledCli(f, 'doctor', '--full');
153
+ expect([0, 1]).toContain(result.code);
154
+ expect(result.stdout).toContain('Processes');
155
+ // El adapter de producción (productionDashboardAdapters.processes, src/core/dashboard/collect.ts)
156
+ // construye el id de item como `process.<name>` vía sanitizeDashboardId — esto es lo que
157
+ // demuestra que la sección viene del adapter real (discoverProcessModels() -> Dashboard),
158
+ // no de un parser propio del Dashboard.
159
+ expect(result.stdout).toContain('process.ejemplo-proceso');
160
+ });
161
+ });