agentic-workflow-manager 9.1.0 → 9.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/commands/context/index.js +47 -0
- package/dist/src/commands/process/index.js +9 -32
- package/dist/src/commands/update.js +24 -2
- package/dist/src/core/command-result.js +17 -0
- package/dist/src/core/context/provider.js +27 -18
- package/dist/src/core/orchestrators.js +24 -3
- package/dist/src/core/registries.js +18 -2
- package/dist/src/core/text.js +58 -0
- package/dist/src/core/versioning.js +2 -2
- package/dist/src/index.js +2 -0
- package/dist/tests/commands/context.test.js +64 -0
- package/dist/tests/commands/update.test.js +31 -0
- package/dist/tests/core/context/provider.test.js +49 -0
- package/dist/tests/core/orchestrators.test.js +38 -0
- package/dist/tests/core/registries-sync.test.js +29 -11
- package/dist/tests/core/text.test.js +29 -0
- package/dist/tests/core/versioning.test.js +5 -5
- package/dist/tests/integration/context-orchestrators-e2e.test.js +144 -0
- package/package.json +1 -1
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runContextOrchestrators = runContextOrchestrators;
|
|
4
|
+
exports.registerContextCommand = registerContextCommand;
|
|
5
|
+
const orchestrators_1 = require("../../core/orchestrators");
|
|
6
|
+
const provider_1 = require("../../core/context/provider");
|
|
7
|
+
const text_1 = require("../../core/text");
|
|
8
|
+
const command_result_1 = require("../../core/command-result");
|
|
9
|
+
function runContextOrchestrators(collected, opts) {
|
|
10
|
+
const stderr = (0, command_result_1.diagnosticsToStderr)(collected.diagnostics);
|
|
11
|
+
const composed = (0, provider_1.composedOrchestrators)(collected.declared);
|
|
12
|
+
if (opts.verify !== undefined) {
|
|
13
|
+
// opts.verify es el argumento CRUDO tal como lo tipeo el usuario; composed[].name
|
|
14
|
+
// ya paso por sanitizeDeclaredField. Sin normalizar este lado con la MISMA funcion,
|
|
15
|
+
// un usuario que tipee el nombre declarado exacto (con un caracter que el saneo
|
|
16
|
+
// quita, p.ej. `_` o un byte de control) se lleva un falso "not composed" (R3.5/R3.6).
|
|
17
|
+
const verifyNormalized = (0, text_1.sanitizeDeclaredField)(opts.verify);
|
|
18
|
+
const found = composed.some((o) => o.name === verifyNormalized);
|
|
19
|
+
if (!found) {
|
|
20
|
+
const available = composed.map((o) => o.name).join(', ') || '(none)';
|
|
21
|
+
return {
|
|
22
|
+
code: 2, stdout: '',
|
|
23
|
+
stderr: `${stderr}awm context orchestrators: "${verifyNormalized}" is not composed — available: ${available}\n`,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
return { code: 0, stdout: `"${verifyNormalized}" is composed into the session context.\n`, stderr };
|
|
27
|
+
}
|
|
28
|
+
if (opts.json) {
|
|
29
|
+
return { code: 0, stdout: `${JSON.stringify({ orchestrators: composed }, null, 2)}\n`, stderr };
|
|
30
|
+
}
|
|
31
|
+
if (composed.length === 0) {
|
|
32
|
+
return { code: 0, stdout: 'No declared orchestrators in the installed registries.\n', stderr };
|
|
33
|
+
}
|
|
34
|
+
const rows = composed
|
|
35
|
+
.map((o) => `${o.name} applies when: ${o.appliesWhen} -> ${o.terminatesTo}`)
|
|
36
|
+
.join('\n');
|
|
37
|
+
return { code: 0, stdout: `${rows}\n`, stderr };
|
|
38
|
+
}
|
|
39
|
+
function registerContextCommand(program) {
|
|
40
|
+
const context = program.command('context').description('read-only view of the context AWM delivers to every agent session');
|
|
41
|
+
context
|
|
42
|
+
.command('orchestrators')
|
|
43
|
+
.description('list the declared orchestrators as they are composed into the session context')
|
|
44
|
+
.option('--json', 'emit the composed list as JSON')
|
|
45
|
+
.option('--verify <name>', 'exit 0 only if that orchestrator is composed; 2 otherwise')
|
|
46
|
+
.action((opts) => (0, command_result_1.emit)(runContextOrchestrators((0, orchestrators_1.collectDeclaredOrchestrators)(), { json: opts.json === true, verify: opts.verify })));
|
|
47
|
+
}
|
|
@@ -4,6 +4,8 @@ exports.runProcessList = runProcessList;
|
|
|
4
4
|
exports.runProcessShow = runProcessShow;
|
|
5
5
|
exports.registerProcessCommand = registerProcessCommand;
|
|
6
6
|
const discover_1 = require("../../core/process/discover");
|
|
7
|
+
const text_1 = require("../../core/text");
|
|
8
|
+
const command_result_1 = require("../../core/command-result");
|
|
7
9
|
/** `source` es un path absoluto del filesystem local: identifica la máquina, no
|
|
8
10
|
* el proceso. Se usa internamente para diagnósticos, y se omite de toda salida
|
|
9
11
|
* destinada a ser compartida o consumida por otro programa. */
|
|
@@ -11,25 +13,8 @@ function publicView(model) {
|
|
|
11
13
|
const { source: _source, ...rest } = model;
|
|
12
14
|
return rest;
|
|
13
15
|
}
|
|
14
|
-
/** Formatea los diagnósticos de descubrimiento como líneas `warning: ...\n`
|
|
15
|
-
* listas para stderr. Compartido por list y show para no divergir el formato. */
|
|
16
|
-
function diagnosticsToStderr(diagnostics) {
|
|
17
|
-
return diagnostics.map((d) => `warning: ${d}\n`).join('');
|
|
18
|
-
}
|
|
19
|
-
/** Neutraliza bytes de control C0 (incluyendo ESC `\x1b`) de texto de body
|
|
20
|
-
* proveniente de un registry no confiable antes de escribirlo a una terminal
|
|
21
|
-
* humana. `\n` y `\t` se preservan porque son whitespace legítimo usado por
|
|
22
|
-
* la vista de texto; el resto de la vista JSON no pasa por acá — `JSON.stringify`
|
|
23
|
-
* ya escapa los caracteres de control como parte de producir JSON válido. Esto
|
|
24
|
-
* es un problema distinto y más simple que `sanitizeDashboardSource` (Dashboard,
|
|
25
|
-
* Task 5): ahí se canonicaliza ids/labels contra un vocabulario permitido; acá
|
|
26
|
-
* solo se neutraliza ANSI/control crudo antes de escribir a stdout. */
|
|
27
|
-
function stripControlChars(text) {
|
|
28
|
-
// eslint-disable-next-line no-control-regex -- necesitamos matchear C0 deliberadamente
|
|
29
|
-
return text.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '');
|
|
30
|
-
}
|
|
31
16
|
function runProcessList(discovered) {
|
|
32
|
-
const stderr = diagnosticsToStderr(discovered.diagnostics);
|
|
17
|
+
const stderr = (0, command_result_1.diagnosticsToStderr)(discovered.diagnostics);
|
|
33
18
|
if (discovered.models.length === 0) {
|
|
34
19
|
return { code: 0, stdout: 'No process models declared by the installed registries.\n', stderr };
|
|
35
20
|
}
|
|
@@ -39,7 +24,7 @@ function runProcessList(discovered) {
|
|
|
39
24
|
return { code: 0, stdout: `${rows}\n`, stderr };
|
|
40
25
|
}
|
|
41
26
|
function runProcessShow(discovered, name, json) {
|
|
42
|
-
const stderr = diagnosticsToStderr(discovered.diagnostics);
|
|
27
|
+
const stderr = (0, command_result_1.diagnosticsToStderr)(discovered.diagnostics);
|
|
43
28
|
const found = discovered.models.find((m) => m.name === name);
|
|
44
29
|
if (!found) {
|
|
45
30
|
const available = discovered.models.map((m) => m.name).join(', ') || '(none)';
|
|
@@ -52,10 +37,10 @@ function runProcessShow(discovered, name, json) {
|
|
|
52
37
|
const view = publicView(found);
|
|
53
38
|
const structure = view.body.structure
|
|
54
39
|
.map((sg) => [
|
|
55
|
-
`${stripControlChars(sg.id)} — ${stripControlChars(sg.text)}`,
|
|
56
|
-
...sg.operations.map((op) => ` ${stripControlChars(op.id)} — ${stripControlChars(op.text)}`),
|
|
40
|
+
`${(0, text_1.stripControlChars)(sg.id)} — ${(0, text_1.stripControlChars)(sg.text)}`,
|
|
41
|
+
...sg.operations.map((op) => ` ${(0, text_1.stripControlChars)(op.id)} — ${(0, text_1.stripControlChars)(op.text)}`),
|
|
57
42
|
].join('\n')).join('\n');
|
|
58
|
-
return { code: 0, stdout: `${view.name} (${view.status})\n\n${stripControlChars(view.body.objective)}\n\n${structure}\n`, stderr };
|
|
43
|
+
return { code: 0, stdout: `${view.name} (${view.status})\n\n${(0, text_1.stripControlChars)(view.body.objective)}\n\n${structure}\n`, stderr };
|
|
59
44
|
}
|
|
60
45
|
return { code: 0, stdout: `${JSON.stringify(publicView(found), null, 2)}\n`, stderr };
|
|
61
46
|
}
|
|
@@ -64,18 +49,10 @@ function registerProcessCommand(program) {
|
|
|
64
49
|
process_
|
|
65
50
|
.command('list')
|
|
66
51
|
.description('list process models declared by the installed registries')
|
|
67
|
-
.action(() => emit(runProcessList((0, discover_1.discoverProcessModels)())));
|
|
52
|
+
.action(() => (0, command_result_1.emit)(runProcessList((0, discover_1.discoverProcessModels)())));
|
|
68
53
|
process_
|
|
69
54
|
.command('show <name>')
|
|
70
55
|
.description('show one process model')
|
|
71
56
|
.option('--json', 'emit the parsed model as JSON')
|
|
72
|
-
.action((name, opts) => emit(runProcessShow((0, discover_1.discoverProcessModels)(), name, opts.json === true)));
|
|
73
|
-
}
|
|
74
|
-
function emit(result) {
|
|
75
|
-
if (result.stderr)
|
|
76
|
-
process.stderr.write(result.stderr);
|
|
77
|
-
if (result.stdout)
|
|
78
|
-
process.stdout.write(result.stdout);
|
|
79
|
-
if (result.code !== 0)
|
|
80
|
-
process.exitCode = result.code;
|
|
57
|
+
.action((name, opts) => (0, command_result_1.emit)(runProcessShow((0, discover_1.discoverProcessModels)(), name, opts.json === true)));
|
|
81
58
|
}
|
|
@@ -21,6 +21,7 @@ exports.runUpdateCore = runUpdateCore;
|
|
|
21
21
|
// instead (which already imports clack at the top and is never `require()`d
|
|
22
22
|
// by tests).
|
|
23
23
|
const picocolors_1 = __importDefault(require("picocolors"));
|
|
24
|
+
const versioning_1 = require("../core/versioning");
|
|
24
25
|
const paths_1 = require("../core/paths");
|
|
25
26
|
const config_1 = require("../utils/config");
|
|
26
27
|
const agent_targets_1 = require("../core/agent-targets");
|
|
@@ -89,8 +90,29 @@ async function runUpdateCore(options = {}, deps = {}) {
|
|
|
89
90
|
for (const r of registryResults) {
|
|
90
91
|
if (r.action === 'error')
|
|
91
92
|
console.warn(picocolors_1.default.yellow(` ⚠ registry ${r.name}: ${r.error}`));
|
|
92
|
-
else
|
|
93
|
-
|
|
93
|
+
else if (r.disposition === 'regressed') {
|
|
94
|
+
const latest = r.availableVersion && r.availableVersion !== r.version
|
|
95
|
+
? ` Latest available: ${r.availableVersion}.`
|
|
96
|
+
: '';
|
|
97
|
+
console.warn(picocolors_1.default.yellow(` ⚠ Registry ${r.name} regressed ${r.previousVersion ?? '(previous version)'} → ${r.version} `
|
|
98
|
+
+ `because it is pinned to v${(0, versioning_1.normalizePin)(r.pin ?? '')}.${latest} Run \`awm unpin ${r.name}\` to move forward.`));
|
|
99
|
+
}
|
|
100
|
+
else if (r.pin && r.availableVersion !== r.version) {
|
|
101
|
+
console.log(picocolors_1.default.yellow(` ✓ Registry ${r.name} resolved @ ${r.version} (pinned to v${(0, versioning_1.normalizePin)(r.pin)}; `
|
|
102
|
+
+ `latest available: ${r.availableVersion ?? 'unknown'}). Run \`awm unpin ${r.name}\` to move forward.`));
|
|
103
|
+
}
|
|
104
|
+
else if (r.disposition === 'unchanged') {
|
|
105
|
+
console.log(picocolors_1.default.dim(` ✓ Registry ${r.name} already at ${r.version}`));
|
|
106
|
+
}
|
|
107
|
+
else if (r.disposition === 'advanced') {
|
|
108
|
+
console.log(picocolors_1.default.green(` ✓ Registry ${r.name} updated @ ${r.version}`));
|
|
109
|
+
}
|
|
110
|
+
else if (r.disposition === 'installed') {
|
|
111
|
+
console.log(picocolors_1.default.green(` ✓ Registry ${r.name} installed @ ${r.version}`));
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
console.log(picocolors_1.default.green(` ✓ Registry ${r.name} resolved @ ${r.version}`));
|
|
115
|
+
}
|
|
94
116
|
}
|
|
95
117
|
const registries = {
|
|
96
118
|
configured: registryResults.length,
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.diagnosticsToStderr = diagnosticsToStderr;
|
|
4
|
+
exports.emit = emit;
|
|
5
|
+
/** Formatea diagnósticos como líneas `warning: ...\n` listas para stderr.
|
|
6
|
+
* Compartido entre comandos para no divergir el formato. */
|
|
7
|
+
function diagnosticsToStderr(diagnostics) {
|
|
8
|
+
return diagnostics.map((d) => `warning: ${d}\n`).join('');
|
|
9
|
+
}
|
|
10
|
+
function emit(result) {
|
|
11
|
+
if (result.stderr)
|
|
12
|
+
process.stderr.write(result.stderr);
|
|
13
|
+
if (result.stdout)
|
|
14
|
+
process.stdout.write(result.stdout);
|
|
15
|
+
if (result.code !== 0)
|
|
16
|
+
process.exitCode = result.code;
|
|
17
|
+
}
|
|
@@ -4,11 +4,13 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.sha256 = sha256;
|
|
7
|
+
exports.composedOrchestrators = composedOrchestrators;
|
|
7
8
|
exports.buildContext = buildContext;
|
|
8
9
|
// cli/src/core/context/provider.ts
|
|
9
10
|
const crypto_1 = __importDefault(require("crypto"));
|
|
10
11
|
const fs_1 = __importDefault(require("fs"));
|
|
11
12
|
const path_1 = __importDefault(require("path"));
|
|
13
|
+
const text_1 = require("../text");
|
|
12
14
|
function sha256(input) {
|
|
13
15
|
return crypto_1.default.createHash('sha256').update(input, 'utf-8').digest('hex');
|
|
14
16
|
}
|
|
@@ -17,29 +19,36 @@ function parseVersion(skill) {
|
|
|
17
19
|
return m ? m[1].trim() : '0.0.0';
|
|
18
20
|
}
|
|
19
21
|
/**
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
22
|
+
* La lista de orquestadores declarados TAL COMO entra al payload de contexto:
|
|
23
|
+
* saneada campo por campo con `sanitizeDeclaredField` (core/text.ts) — que
|
|
24
|
+
* neutraliza tanto marcado markdown/saltos de linea (prompt-injection via
|
|
25
|
+
* `##`, `` ` ``, `*`, `_`, `<`, `>`) COMO bytes de control C0/DEL (ANSI
|
|
26
|
+
* escapes hacia una terminal real). `readDeclaredOrchestrators` solo valida
|
|
27
|
+
* que los campos sean strings no vacios; el saneo pertenece a esta frontera
|
|
28
|
+
* de render, no a la validacion de lectura.
|
|
29
|
+
*
|
|
30
|
+
* ESTA es la UNICA funcion que hace este saneo — es la que consume
|
|
31
|
+
* `renderDeclared` (el payload materializado en cada sesion de agente via
|
|
32
|
+
* `buildContext`) y la que consume `awm context orchestrators` (comando).
|
|
33
|
+
* Ningun consumidor necesita sanear de nuevo por su cuenta: si el comando
|
|
34
|
+
* aplicara su propio saneo, comando y payload podrian divergir en silencio
|
|
35
|
+
* — el modo de falla que R5.2 prohíbe para el modelo de proceso y que vale
|
|
36
|
+
* igual acá. Por la misma razon, cualquier comparacion contra un nombre
|
|
37
|
+
* declarado (p.ej. `--verify` en `commands/context/index.ts`) debe normalizar
|
|
38
|
+
* su lado tambien con `sanitizeDeclaredField`, no con una copia del regex.
|
|
29
39
|
*/
|
|
30
|
-
function
|
|
31
|
-
return
|
|
40
|
+
function composedOrchestrators(list) {
|
|
41
|
+
return list.map(o => ({
|
|
42
|
+
name: (0, text_1.sanitizeDeclaredField)(o.name),
|
|
43
|
+
appliesWhen: (0, text_1.sanitizeDeclaredField)(o.appliesWhen),
|
|
44
|
+
terminatesTo: (0, text_1.sanitizeDeclaredField)(o.terminatesTo),
|
|
45
|
+
}));
|
|
32
46
|
}
|
|
33
47
|
function renderDeclared(list) {
|
|
34
48
|
if (list.length === 0)
|
|
35
49
|
return '';
|
|
36
|
-
const rows = list
|
|
37
|
-
.map(o => {
|
|
38
|
-
const name = sanitizeForMarkdown(o.name);
|
|
39
|
-
const appliesWhen = sanitizeForMarkdown(o.appliesWhen);
|
|
40
|
-
const terminatesTo = sanitizeForMarkdown(o.terminatesTo);
|
|
41
|
-
return `- **${name}** — applies when: ${appliesWhen}. Terminates to: \`${terminatesTo}\`.`;
|
|
42
|
-
})
|
|
50
|
+
const rows = composedOrchestrators(list)
|
|
51
|
+
.map(o => `- **${o.name}** — applies when: ${o.appliesWhen}. Terminates to: \`${o.terminatesTo}\`.`)
|
|
43
52
|
.join('\n');
|
|
44
53
|
return `## Declared orchestrators\n\nConsider these before the built-in pair:\n\n${rows}\n\n`;
|
|
45
54
|
}
|
|
@@ -17,6 +17,7 @@ exports.collectAndWarn = collectAndWarn;
|
|
|
17
17
|
const fs_1 = __importDefault(require("fs"));
|
|
18
18
|
const path_1 = __importDefault(require("path"));
|
|
19
19
|
const registries_1 = require("./registries");
|
|
20
|
+
const text_1 = require("./text");
|
|
20
21
|
const ALLOWED_FIELDS = ['name', 'appliesWhen', 'terminatesTo'];
|
|
21
22
|
// These fields are semantically short (a short identity, a short trigger condition, a
|
|
22
23
|
// short target name) — no legitimate declaration needs more than this. A registry is
|
|
@@ -109,21 +110,41 @@ function readDeclaredOrchestrators(root) {
|
|
|
109
110
|
* de emitir ambas filas al markdown compuesto, gana la primera en el orden de
|
|
110
111
|
* listRegistries() (= orden de registries.json, ver registries.ts) y la duplicada se
|
|
111
112
|
* descarta con un diagnostico — misma degradacion tolerante (reportar, no lanzar) que el
|
|
112
|
-
* resto de este modulo (R1.2, R5.1).
|
|
113
|
+
* resto de este modulo (R1.2, R5.1). `orch.name` es contenido no confiable del registry:
|
|
114
|
+
* se pasa por `stripControlChars` (core/text.ts) antes de interpolarlo en el diagnostico,
|
|
115
|
+
* que termina crudo en stderr real via `command-result.ts`'s `diagnosticsToStderr`/`emit`.
|
|
116
|
+
*
|
|
117
|
+
* Ademas del dedupe por nombre CRUDO, dos orquestadores con nombres crudos distintos
|
|
118
|
+
* (p.ej. `foo_bar` y `foo*bar`) pueden colisionar DESPUES del saneo que aplica
|
|
119
|
+
* `composedOrchestrators` (core/context/provider.ts) — ambos renderizarian como el mismo
|
|
120
|
+
* `foobar` visible, sin explicacion. Como son declaraciones genuinamente distintas, no se
|
|
121
|
+
* descarta ninguna (eso perderia una declaracion real) — se emite un diagnostico
|
|
122
|
+
* accionable en su lugar. `sanitizeDeclaredField` vive en core/text.ts (no en provider.ts)
|
|
123
|
+
* precisamente para que este modulo -- que debe seguir siendo hoja y no puede importar
|
|
124
|
+
* context/provider.ts, el cual ya importa `DeclaredOrchestrator` DESDE este archivo -- pueda
|
|
125
|
+
* calcular el mismo nombre saneado sin duplicar el regex.
|
|
113
126
|
*/
|
|
114
127
|
function collectDeclaredOrchestrators() {
|
|
115
128
|
const declared = [];
|
|
116
129
|
const diagnostics = [];
|
|
117
130
|
const seenNames = new Set();
|
|
131
|
+
const seenSanitizedNames = new Set();
|
|
118
132
|
for (const reg of (0, registries_1.listRegistries)()) {
|
|
119
133
|
const r = readDeclaredOrchestrators(reg.contentRoot);
|
|
120
134
|
for (const orch of r.orchestrators) {
|
|
135
|
+
const file = path_1.default.join(reg.contentRoot, registries_1.REGISTRY_MANIFEST_NAME);
|
|
121
136
|
if (seenNames.has(orch.name)) {
|
|
122
|
-
|
|
123
|
-
diagnostics.push(`${file}: orchestrator "${orch.name}" duplicates one already declared by an earlier registry — shadowed duplicate dropped`);
|
|
137
|
+
diagnostics.push(`${file}: orchestrator "${(0, text_1.stripControlChars)(orch.name)}" duplicates one already declared by an earlier registry — shadowed duplicate dropped`);
|
|
124
138
|
continue;
|
|
125
139
|
}
|
|
126
140
|
seenNames.add(orch.name);
|
|
141
|
+
const sanitizedName = (0, text_1.sanitizeDeclaredField)(orch.name);
|
|
142
|
+
if (seenSanitizedNames.has(sanitizedName)) {
|
|
143
|
+
diagnostics.push(`${file}: orchestrator "${(0, text_1.stripControlChars)(orch.name)}" sanitizes to the same composed name "${sanitizedName}" as an already declared orchestrator — both will render as indistinguishable duplicates in the composed context`);
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
seenSanitizedNames.add(sanitizedName);
|
|
147
|
+
}
|
|
127
148
|
declared.push(orch);
|
|
128
149
|
}
|
|
129
150
|
diagnostics.push(...r.diagnostics);
|
|
@@ -251,9 +251,11 @@ async function syncRegistries() {
|
|
|
251
251
|
await (0, simple_git_1.default)(reg.contentRoot).reset(['--hard']);
|
|
252
252
|
}
|
|
253
253
|
const git = (0, simple_git_1.default)(reg.contentRoot);
|
|
254
|
+
const previousVersion = freshClone ? null : await (0, versioning_1.currentVersion)(reg.contentRoot);
|
|
255
|
+
const versionOpts = (0, versioning_1.machineVersionOpts)(reg.name);
|
|
254
256
|
let resolved;
|
|
255
257
|
try {
|
|
256
|
-
resolved = await (0, versioning_1.resolveTargetRef)(reg.contentRoot,
|
|
258
|
+
resolved = await (0, versioning_1.resolveTargetRef)(reg.contentRoot, versionOpts);
|
|
257
259
|
await git.checkout(resolved.ref);
|
|
258
260
|
if (resolved.kind !== 'tag')
|
|
259
261
|
await git.pull('origin', resolved.ref);
|
|
@@ -266,10 +268,24 @@ async function syncRegistries() {
|
|
|
266
268
|
fs_1.default.rmSync(reg.contentRoot, { recursive: true, force: true });
|
|
267
269
|
throw e;
|
|
268
270
|
}
|
|
271
|
+
const version = resolved.kind === 'tag' ? `v${resolved.version}` : 'HEAD';
|
|
272
|
+
let disposition;
|
|
273
|
+
if (freshClone)
|
|
274
|
+
disposition = 'installed';
|
|
275
|
+
else if (resolved.kind !== 'tag' || previousVersion === null)
|
|
276
|
+
disposition = 'resolved';
|
|
277
|
+
else {
|
|
278
|
+
const direction = (0, versioning_1.compareSemver)(resolved.version, previousVersion);
|
|
279
|
+
disposition = direction > 0 ? 'advanced' : direction < 0 ? 'regressed' : 'unchanged';
|
|
280
|
+
}
|
|
269
281
|
results.push({
|
|
270
282
|
name: reg.name,
|
|
271
283
|
action: freshClone ? 'recloned' : 'pulled',
|
|
272
|
-
version
|
|
284
|
+
version,
|
|
285
|
+
disposition,
|
|
286
|
+
...(versionOpts.pin ? { pin: (0, versioning_1.normalizePin)(versionOpts.pin) } : {}),
|
|
287
|
+
...(previousVersion ? { previousVersion: `v${previousVersion}` } : {}),
|
|
288
|
+
...(resolved.kind === 'tag' ? { availableVersion: `v${resolved.latestVersion}` } : {}),
|
|
273
289
|
});
|
|
274
290
|
}
|
|
275
291
|
catch (e) {
|
|
@@ -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
|
+
}
|
|
@@ -69,7 +69,7 @@ async function resolveTargetRef(repoDir, opts) {
|
|
|
69
69
|
: 'the registry has no version tags';
|
|
70
70
|
throw new Error(`Pinned version ${want} not found in ${repoDir} — ${available}`);
|
|
71
71
|
}
|
|
72
|
-
return { kind: 'tag', ref: want, version: normalizePin(opts.pin) };
|
|
72
|
+
return { kind: 'tag', ref: want, version: normalizePin(opts.pin), latestVersion: tags[tags.length - 1].slice(1) };
|
|
73
73
|
}
|
|
74
74
|
const branch = await defaultBranch(git);
|
|
75
75
|
if (opts.channel === 'dev')
|
|
@@ -77,7 +77,7 @@ async function resolveTargetRef(repoDir, opts) {
|
|
|
77
77
|
if (tags.length === 0)
|
|
78
78
|
return { kind: 'head-fallback', ref: branch };
|
|
79
79
|
const latest = tags[tags.length - 1];
|
|
80
|
-
return { kind: 'tag', ref: latest, version: latest.slice(1) };
|
|
80
|
+
return { kind: 'tag', ref: latest, version: latest.slice(1), latestVersion: latest.slice(1) };
|
|
81
81
|
}
|
|
82
82
|
/** Versión checkouteada actual ("X.Y.Z") si HEAD coincide exactamente con un tag semver; null si sigue un branch. */
|
|
83
83
|
async function currentVersion(repoDir) {
|
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
|
+
});
|
|
@@ -125,6 +125,37 @@ describe('awm update — honest outcome', () => {
|
|
|
125
125
|
expect(result.registries).toEqual({ configured: 2, synced: 2, failed: [] });
|
|
126
126
|
expect(updateOutro(result)).toBe('✅ 2 registries, skills and hooks updated.');
|
|
127
127
|
});
|
|
128
|
+
it('una regresión causada por pin se advierte y explica cómo volver a avanzar', async () => {
|
|
129
|
+
const { runUpdateCore } = require('../../src/commands/update');
|
|
130
|
+
await runUpdateCore({}, deps({
|
|
131
|
+
syncRegistries: async () => [{
|
|
132
|
+
name: 'baseline',
|
|
133
|
+
action: 'pulled',
|
|
134
|
+
version: 'v1.0.0',
|
|
135
|
+
disposition: 'regressed',
|
|
136
|
+
pin: '1.0.0',
|
|
137
|
+
availableVersion: 'v1.1.0',
|
|
138
|
+
}],
|
|
139
|
+
}));
|
|
140
|
+
const output = [...logSpy.mock.calls, ...warnSpy.mock.calls].flat().join('\n');
|
|
141
|
+
expect(output).toMatch(/regress/i);
|
|
142
|
+
expect(output).toMatch(/v1\.0\.0/);
|
|
143
|
+
expect(output).toMatch(/v1\.1\.0/);
|
|
144
|
+
expect(output).toMatch(/awm unpin baseline/);
|
|
145
|
+
expect(output).not.toMatch(/Registry baseline updated/i);
|
|
146
|
+
});
|
|
147
|
+
it('un pin que ya coincide con el último release no recomienda unpin', async () => {
|
|
148
|
+
const { runUpdateCore } = require('../../src/commands/update');
|
|
149
|
+
await runUpdateCore({}, deps({
|
|
150
|
+
syncRegistries: async () => [{
|
|
151
|
+
name: 'baseline', action: 'pulled', version: 'v1.1.0', disposition: 'unchanged',
|
|
152
|
+
pin: 'v1.1.0', availableVersion: 'v1.1.0',
|
|
153
|
+
}],
|
|
154
|
+
}));
|
|
155
|
+
const output = [...logSpy.mock.calls, ...warnSpy.mock.calls].flat().join('\n');
|
|
156
|
+
expect(output).toMatch(/already at v1\.1\.0/i);
|
|
157
|
+
expect(output).not.toMatch(/awm unpin baseline/);
|
|
158
|
+
});
|
|
128
159
|
it('un registry que falló y NO dejó contenido en disco falla cerrado', async () => {
|
|
129
160
|
const { runUpdateCore } = require('../../src/commands/update');
|
|
130
161
|
const result = await runUpdateCore({}, deps({
|
|
@@ -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' },
|
|
@@ -79,7 +79,7 @@ describe('syncRegistries (git fixtures locales)', () => {
|
|
|
79
79
|
const source = makeSourceRepo(tmpWork, 'alpha');
|
|
80
80
|
m.writeRegistriesConfig([{ name: 'personal', remote: source }]);
|
|
81
81
|
const results = await m.syncRegistries();
|
|
82
|
-
expect(results).toEqual([{ name: 'personal', action: 'recloned', version: 'HEAD' }]);
|
|
82
|
+
expect(results).toEqual([{ name: 'personal', action: 'recloned', version: 'HEAD', disposition: 'installed' }]);
|
|
83
83
|
expect(fs_1.default.existsSync(path_1.default.join(tmpHome, '.awm/registries/personal/skills/alpha/SKILL.md'))).toBe(true);
|
|
84
84
|
});
|
|
85
85
|
it('pulls an existing clone and reports non-fatal errors per registry', async () => {
|
|
@@ -95,7 +95,7 @@ describe('syncRegistries (git fixtures locales)', () => {
|
|
|
95
95
|
GIT(source, 'add -A');
|
|
96
96
|
GIT(source, 'commit -qm v2');
|
|
97
97
|
const results = await m.syncRegistries();
|
|
98
|
-
expect(results[0]).toEqual({ name: 'personal', action: 'pulled', version: 'HEAD' });
|
|
98
|
+
expect(results[0]).toEqual({ name: 'personal', action: 'pulled', version: 'HEAD', disposition: 'resolved' });
|
|
99
99
|
expect(results[1].name).toBe('broken');
|
|
100
100
|
expect(results[1].action).toBe('error');
|
|
101
101
|
const synced = fs_1.default.readFileSync(path_1.default.join(tmpHome, '.awm/registries/personal/skills/alpha/SKILL.md'), 'utf-8');
|
|
@@ -127,7 +127,7 @@ describe('syncRegistries (git fixtures locales)', () => {
|
|
|
127
127
|
GIT(source, 'commit -qm unreleased');
|
|
128
128
|
m.writeRegistriesConfig([{ name: 'personal', remote: source }]);
|
|
129
129
|
const results = await m.syncRegistries();
|
|
130
|
-
expect(results).toEqual([{ name: 'personal', action: 'recloned', version: 'v1.0.0' }]);
|
|
130
|
+
expect(results).toEqual([{ name: 'personal', action: 'recloned', version: 'v1.0.0', disposition: 'installed', availableVersion: 'v1.0.0' }]);
|
|
131
131
|
const synced = fs_1.default.readFileSync(path_1.default.join(tmpHome, '.awm/registries/personal/skills/alpha/SKILL.md'), 'utf-8');
|
|
132
132
|
expect(synced).toContain('test skill'); // contenido del tag, no del HEAD
|
|
133
133
|
});
|
|
@@ -157,7 +157,9 @@ describe('syncRegistries (git fixtures locales)', () => {
|
|
|
157
157
|
fs_1.default.mkdirSync(awmDir, { recursive: true });
|
|
158
158
|
fs_1.default.writeFileSync(path_1.default.join(awmDir, 'preferences.json'), JSON.stringify({ defaultAgent: 'claude-code', installMethod: 'symlink', defaultScope: 'local', pins: { personal: '1.0.0' } }));
|
|
159
159
|
const results = await m.syncRegistries();
|
|
160
|
-
expect(results).toEqual([{
|
|
160
|
+
expect(results).toEqual([{
|
|
161
|
+
name: 'personal', action: 'recloned', version: 'v1.0.0', disposition: 'installed', pin: '1.0.0', availableVersion: 'v1.1.0',
|
|
162
|
+
}]);
|
|
161
163
|
});
|
|
162
164
|
it('baseline sembrado se sincroniza por el mismo loop que cualquier registry', async () => {
|
|
163
165
|
const m = require('../../src/core/registries');
|
|
@@ -167,7 +169,7 @@ describe('syncRegistries (git fixtures locales)', () => {
|
|
|
167
169
|
m.seedBaselineRegistry();
|
|
168
170
|
delete process.env.AWM_BASE_REMOTE;
|
|
169
171
|
const results = await m.syncRegistries();
|
|
170
|
-
expect(results).toEqual([{ name: 'baseline', action: 'recloned', version: 'v1.0.0' }]);
|
|
172
|
+
expect(results).toEqual([{ name: 'baseline', action: 'recloned', version: 'v1.0.0', disposition: 'installed', availableVersion: 'v1.0.0' }]);
|
|
171
173
|
expect(fs_1.default.existsSync(path_1.default.join(tmpHome, '.awm/registries/baseline/skills/alpha/SKILL.md'))).toBe(true);
|
|
172
174
|
});
|
|
173
175
|
it('verifyMinCliVersions reporta registries que exigen CLI más nuevo', async () => {
|
|
@@ -193,7 +195,7 @@ describe('syncRegistries (git fixtures locales)', () => {
|
|
|
193
195
|
m.writeRegistriesConfig([{ name: 'personal', remote: source }]);
|
|
194
196
|
// primera sync: queda en v1.0.0
|
|
195
197
|
const r1 = await m.syncRegistries();
|
|
196
|
-
expect(r1).toEqual([{ name: 'personal', action: 'recloned', version: 'v1.0.0' }]);
|
|
198
|
+
expect(r1).toEqual([{ name: 'personal', action: 'recloned', version: 'v1.0.0', disposition: 'installed', availableVersion: 'v1.0.0' }]);
|
|
197
199
|
// nueva release en el remote
|
|
198
200
|
fs_1.default.writeFileSync(path_1.default.join(source, 'skills', 'alpha', 'SKILL.md'), '---\nname: alpha\ndescription: v2\n---\n');
|
|
199
201
|
GIT(source, 'add -A');
|
|
@@ -201,9 +203,15 @@ describe('syncRegistries (git fixtures locales)', () => {
|
|
|
201
203
|
GIT(source, 'tag v1.1.0');
|
|
202
204
|
// segunda sync: avanza al nuevo tag
|
|
203
205
|
const r2 = await m.syncRegistries();
|
|
204
|
-
expect(r2).toEqual([{
|
|
206
|
+
expect(r2).toEqual([{
|
|
207
|
+
name: 'personal', action: 'pulled', version: 'v1.1.0', disposition: 'advanced', previousVersion: 'v1.0.0', availableVersion: 'v1.1.0',
|
|
208
|
+
}]);
|
|
205
209
|
const content = fs_1.default.readFileSync(path_1.default.join(tmpHome, '.awm/registries/personal/skills/alpha/SKILL.md'), 'utf-8');
|
|
206
210
|
expect(content).toContain('v2');
|
|
211
|
+
const r3 = await m.syncRegistries();
|
|
212
|
+
expect(r3).toEqual([{
|
|
213
|
+
name: 'personal', action: 'pulled', version: 'v1.1.0', disposition: 'unchanged', previousVersion: 'v1.1.0', availableVersion: 'v1.1.0',
|
|
214
|
+
}]);
|
|
207
215
|
});
|
|
208
216
|
it('rollback tag→tag: clone en v1.1.0 retrocede a v1.0.0 al establecer pin', async () => {
|
|
209
217
|
const m = require('../../src/core/registries');
|
|
@@ -216,13 +224,21 @@ describe('syncRegistries (git fixtures locales)', () => {
|
|
|
216
224
|
m.writeRegistriesConfig([{ name: 'personal', remote: source }]);
|
|
217
225
|
// primera sync sin pin: queda en v1.1.0
|
|
218
226
|
const r1 = await m.syncRegistries();
|
|
219
|
-
expect(r1).toEqual([{ name: 'personal', action: 'recloned', version: 'v1.1.0' }]);
|
|
227
|
+
expect(r1).toEqual([{ name: 'personal', action: 'recloned', version: 'v1.1.0', disposition: 'installed', availableVersion: 'v1.1.0' }]);
|
|
220
228
|
// establece pin a la versión anterior
|
|
221
229
|
const awmDir = path_1.default.join(tmpHome, '.awm');
|
|
222
230
|
fs_1.default.writeFileSync(path_1.default.join(awmDir, 'preferences.json'), JSON.stringify({ defaultAgent: 'claude-code', installMethod: 'symlink', defaultScope: 'local', pins: { personal: '1.0.0' } }));
|
|
223
231
|
// segunda sync con pin: retrocede a v1.0.0
|
|
224
232
|
const r2 = await m.syncRegistries();
|
|
225
|
-
expect(r2).toEqual([{
|
|
233
|
+
expect(r2).toEqual([{
|
|
234
|
+
name: 'personal',
|
|
235
|
+
action: 'pulled',
|
|
236
|
+
version: 'v1.0.0',
|
|
237
|
+
disposition: 'regressed',
|
|
238
|
+
pin: '1.0.0',
|
|
239
|
+
previousVersion: 'v1.1.0',
|
|
240
|
+
availableVersion: 'v1.1.0',
|
|
241
|
+
}]);
|
|
226
242
|
const content = fs_1.default.readFileSync(path_1.default.join(tmpHome, '.awm/registries/personal/skills/alpha/SKILL.md'), 'utf-8');
|
|
227
243
|
expect(content).toContain('test skill'); // contenido original del tag v1.0.0
|
|
228
244
|
});
|
|
@@ -236,14 +252,16 @@ describe('syncRegistries (git fixtures locales)', () => {
|
|
|
236
252
|
fs_1.default.writeFileSync(path_1.default.join(awmDir, 'preferences.json'), JSON.stringify({ defaultAgent: 'claude-code', installMethod: 'symlink', defaultScope: 'local', channel: 'dev' }));
|
|
237
253
|
// primera sync en canal dev: queda en HEAD (no en el tag)
|
|
238
254
|
const r1 = await m.syncRegistries();
|
|
239
|
-
expect(r1).toEqual([{ name: 'personal', action: 'recloned', version: 'HEAD' }]);
|
|
255
|
+
expect(r1).toEqual([{ name: 'personal', action: 'recloned', version: 'HEAD', disposition: 'installed' }]);
|
|
240
256
|
// nuevo commit en el remote (post-tag, no release)
|
|
241
257
|
fs_1.default.writeFileSync(path_1.default.join(source, 'skills', 'alpha', 'SKILL.md'), '---\nname: alpha\ndescription: head-2\n---\n');
|
|
242
258
|
GIT(source, 'add -A');
|
|
243
259
|
GIT(source, 'commit -qm head-2');
|
|
244
260
|
// segunda sync: recibe el nuevo commit
|
|
245
261
|
const r2 = await m.syncRegistries();
|
|
246
|
-
expect(r2).toEqual([{
|
|
262
|
+
expect(r2).toEqual([{
|
|
263
|
+
name: 'personal', action: 'pulled', version: 'HEAD', disposition: 'resolved', previousVersion: 'v1.0.0',
|
|
264
|
+
}]);
|
|
247
265
|
const content = fs_1.default.readFileSync(path_1.default.join(tmpHome, '.awm/registries/personal/skills/alpha/SKILL.md'), 'utf-8');
|
|
248
266
|
expect(content).toContain('head-2');
|
|
249
267
|
});
|
|
@@ -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
|
+
});
|
|
@@ -62,16 +62,16 @@ describe('versioning core', () => {
|
|
|
62
62
|
const clone = cloneOf(source, tmpWork, 'clone');
|
|
63
63
|
const { resolveTargetRef } = require('../../src/core/versioning');
|
|
64
64
|
const r = await resolveTargetRef(clone, { channel: 'stable' });
|
|
65
|
-
expect(r).toEqual({ kind: 'tag', ref: 'v1.10.0', version: '1.10.0' });
|
|
65
|
+
expect(r).toEqual({ kind: 'tag', ref: 'v1.10.0', version: '1.10.0', latestVersion: '1.10.0' });
|
|
66
66
|
});
|
|
67
67
|
it('pin exacto gana, con y sin prefijo v', async () => {
|
|
68
68
|
const source = makeTaggedRepo(tmpWork, 'src', ['1.0.0', '1.1.0']);
|
|
69
69
|
const clone = cloneOf(source, tmpWork, 'clone');
|
|
70
70
|
const { resolveTargetRef } = require('../../src/core/versioning');
|
|
71
71
|
expect(await resolveTargetRef(clone, { pin: '1.0.0', channel: 'stable' }))
|
|
72
|
-
.toEqual({ kind: 'tag', ref: 'v1.0.0', version: '1.0.0' });
|
|
72
|
+
.toEqual({ kind: 'tag', ref: 'v1.0.0', version: '1.0.0', latestVersion: '1.1.0' });
|
|
73
73
|
expect(await resolveTargetRef(clone, { pin: 'v1.0.0', channel: 'stable' }))
|
|
74
|
-
.toEqual({ kind: 'tag', ref: 'v1.0.0', version: '1.0.0' });
|
|
74
|
+
.toEqual({ kind: 'tag', ref: 'v1.0.0', version: '1.0.0', latestVersion: '1.1.0' });
|
|
75
75
|
});
|
|
76
76
|
it('pin inexistente → error que lista las versiones disponibles', async () => {
|
|
77
77
|
const source = makeTaggedRepo(tmpWork, 'src', ['1.0.0']);
|
|
@@ -102,7 +102,7 @@ describe('versioning core', () => {
|
|
|
102
102
|
const clone = cloneOf(source, tmpWork, 'clone');
|
|
103
103
|
const { resolveTargetRef } = require('../../src/core/versioning');
|
|
104
104
|
const r = await resolveTargetRef(clone, { channel: 'stable' });
|
|
105
|
-
expect(r).toEqual({ kind: 'tag', ref: 'v1.0.0', version: '1.0.0' });
|
|
105
|
+
expect(r).toEqual({ kind: 'tag', ref: 'v1.0.0', version: '1.0.0', latestVersion: '1.0.0' });
|
|
106
106
|
});
|
|
107
107
|
it('hace fetch: ve tags creados en el remote después del clone', async () => {
|
|
108
108
|
const source = makeTaggedRepo(tmpWork, 'src', ['1.0.0']);
|
|
@@ -114,7 +114,7 @@ describe('versioning core', () => {
|
|
|
114
114
|
GIT(source, 'tag v1.1.0');
|
|
115
115
|
const { resolveTargetRef } = require('../../src/core/versioning');
|
|
116
116
|
const r = await resolveTargetRef(clone, { channel: 'stable' });
|
|
117
|
-
expect(r).toEqual({ kind: 'tag', ref: 'v1.1.0', version: '1.1.0' });
|
|
117
|
+
expect(r).toEqual({ kind: 'tag', ref: 'v1.1.0', version: '1.1.0', latestVersion: '1.1.0' });
|
|
118
118
|
});
|
|
119
119
|
});
|
|
120
120
|
describe('currentVersion', () => {
|
|
@@ -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
|
+
});
|