agentic-workflow-manager 3.13.6 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/scripts/support-matrix.js +138 -0
- package/dist/src/core/init/mutation-targets.js +23 -1
- package/dist/src/index.js +12 -2
- package/dist/tests/commands/watch/runner.test.js +11 -0
- package/dist/tests/core/init/context-injection-no-project.test.js +3 -3
- package/dist/tests/core/init/mutation-targets-canonical.test.js +81 -0
- package/dist/tests/core/init/mutation-targets.test.js +4 -4
- package/dist/tests/core/project-skill-links.test.js +5 -5
- package/dist/tests/structural/support-matrix-is-current.test.js +100 -0
- package/dist/tests/support/tmp.js +31 -0
- package/package.json +3 -2
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.DOC_PATH = exports.END_MARKER = exports.BEGIN_MARKER = void 0;
|
|
7
|
+
exports.homeRelative = homeRelative;
|
|
8
|
+
exports.renderProviderTables = renderProviderTables;
|
|
9
|
+
exports.spliceGenerated = spliceGenerated;
|
|
10
|
+
// Generador de la matriz de soporte que vive en `docs/support-matrix.md`.
|
|
11
|
+
//
|
|
12
|
+
// Existe porque la tabla escrita a mano ya mintio: decia que Antigravity instalaba en
|
|
13
|
+
// `~/.agents/skills` y `.agents/skills` cuando el codigo dice `~/.gemini/antigravity/skills`
|
|
14
|
+
// y `.agent/skills` (singular), y omitia que es el unico provider con `global_workflows`.
|
|
15
|
+
// Una tabla de soporte que no coincide con el codigo es peor que no tenerla: se cita en
|
|
16
|
+
// una presentacion, se planifica encima, y nadie la vuelve a chequear.
|
|
17
|
+
//
|
|
18
|
+
// El bloque generado esta delimitado por marcadores en el .md, y
|
|
19
|
+
// `tests/structural/support-matrix-is-current.test.ts` regenera y compara — asi que un
|
|
20
|
+
// cambio en `providers/index.ts` que no se refleje en el doc pone la CI en rojo. La
|
|
21
|
+
// verificabilidad no depende de que alguien se acuerde.
|
|
22
|
+
const path_1 = __importDefault(require("path"));
|
|
23
|
+
const providers_1 = require("../src/providers");
|
|
24
|
+
const paths_1 = require("../src/core/paths");
|
|
25
|
+
exports.BEGIN_MARKER = '<!-- BEGIN GENERATED: provider-capabilities -->';
|
|
26
|
+
exports.END_MARKER = '<!-- END GENERATED: provider-capabilities -->';
|
|
27
|
+
/** Cualquier separador → `/`. Las rutas que produce `providers()` traen el separador
|
|
28
|
+
* nativo, y el `home` puede traer otro (en win32, un `HOME` de entorno con `/` frente a
|
|
29
|
+
* un `path.join` que devuelve `\`). Normalizar los dos lados ANTES de compararlos es lo
|
|
30
|
+
* unico que hace la comparacion valida. */
|
|
31
|
+
const toPosix = (p) => p.split(path_1.default.sep).join('/').split('\\').join('/');
|
|
32
|
+
/** Rutas absolutas → `~/…`, con separadores POSIX, para que la tabla sea la misma en
|
|
33
|
+
* cualquier maquina y en cualquier sistema operativo que la regenere.
|
|
34
|
+
*
|
|
35
|
+
* La version anterior comparaba con `startsWith` ANTES de normalizar, asi que en Windows
|
|
36
|
+
* —donde `path.join` devuelve `\` y el home podia venir con `/`— el prefijo no
|
|
37
|
+
* coincidia nunca, no abreviaba nada, y la tabla regenerada ahi no era la misma que la
|
|
38
|
+
* comiteada. Es decir: el documento prometia ser independiente de la maquina y no lo era.
|
|
39
|
+
* Lo encontro la CI de Windows, no el desarrollo en Linux. */
|
|
40
|
+
function homeRelative(p, home) {
|
|
41
|
+
const target = toPosix(p);
|
|
42
|
+
const prefix = toPosix(home);
|
|
43
|
+
return target.startsWith(prefix) ? `~${target.slice(prefix.length)}` : target;
|
|
44
|
+
}
|
|
45
|
+
function cell(value, absent, home) {
|
|
46
|
+
if (value === null)
|
|
47
|
+
return absent;
|
|
48
|
+
return `\`${homeRelative(value, home)}\``;
|
|
49
|
+
}
|
|
50
|
+
/** El tier declarado por la forma de la config — la misma derivacion que `providerTier`
|
|
51
|
+
* en `core/diagnostics/provider-checks.ts`, no una segunda opinion. */
|
|
52
|
+
function tier(c) {
|
|
53
|
+
if (c.hooks)
|
|
54
|
+
return 'hooks-native';
|
|
55
|
+
if (c.injection?.type === 'config-instructions')
|
|
56
|
+
return 'config-managed';
|
|
57
|
+
if (c.injection)
|
|
58
|
+
return 'agents-md-managed';
|
|
59
|
+
return 'context-only';
|
|
60
|
+
}
|
|
61
|
+
function injectionCell(c, home) {
|
|
62
|
+
const inj = c.injection;
|
|
63
|
+
if (!inj)
|
|
64
|
+
return '— (ninguna)';
|
|
65
|
+
if (inj.type === 'cc-settings-merge')
|
|
66
|
+
return 'hook `SessionStart`';
|
|
67
|
+
if (inj.type === 'config-instructions')
|
|
68
|
+
return `\`${homeRelative(inj.configPath, home)}\` → campo \`${inj.field}\``;
|
|
69
|
+
return inj.globalPath === null
|
|
70
|
+
? `\`${inj.localFile}\` del proyecto (sin equivalente global)`
|
|
71
|
+
: `\`${inj.localFile}\` + \`${homeRelative(inj.globalPath, home)}\``;
|
|
72
|
+
}
|
|
73
|
+
function renderProviderTables() {
|
|
74
|
+
// `homeDir()`, la MISMA funcion de la que `providers()` deriva sus rutas. Tomarlo
|
|
75
|
+
// por parametro daba dos fuentes para el mismo dato: el generador abreviaba contra
|
|
76
|
+
// el home real y el test contra uno inventado, asi que ninguna ruta empezaba con el
|
|
77
|
+
// prefijo esperado y la tabla salia con rutas absolutas de la maquina que la corrio.
|
|
78
|
+
const home = (0, paths_1.homeDir)();
|
|
79
|
+
const p = (0, providers_1.providers)();
|
|
80
|
+
const row = (a) => p[a];
|
|
81
|
+
const lines = [];
|
|
82
|
+
lines.push('### Dónde aterriza cada artefacto');
|
|
83
|
+
lines.push('');
|
|
84
|
+
lines.push('| Agente | Tier | Skills (global) | Skills (proyecto) | Formato |');
|
|
85
|
+
lines.push('|---|---|---|---|---|');
|
|
86
|
+
for (const a of providers_1.AGENT_TARGETS) {
|
|
87
|
+
const c = row(a);
|
|
88
|
+
lines.push(`| \`${a}\` | ${tier(c)} | ${cell(c.skill.global, '**no soportado**', home)} | \`${c.skill.local}\` | \`${c.skill.renderer}\` |`);
|
|
89
|
+
}
|
|
90
|
+
lines.push('');
|
|
91
|
+
lines.push('### Perfiles de agente, workflows, hooks y contexto');
|
|
92
|
+
lines.push('');
|
|
93
|
+
lines.push('| Agente | Perfiles de agente | Workflows | Hooks | Entrega de contexto | Versión mínima |');
|
|
94
|
+
lines.push('|---|---|---|---|---|---|');
|
|
95
|
+
for (const a of providers_1.AGENT_TARGETS) {
|
|
96
|
+
const c = row(a);
|
|
97
|
+
const agentCell = c.agent === null
|
|
98
|
+
? '— (no aplica)'
|
|
99
|
+
: `${cell(c.agent.global, '**no soportado**', home)} · \`${c.agent.renderer}\``;
|
|
100
|
+
const wfCell = c.workflow === null
|
|
101
|
+
? '— (no aplica)'
|
|
102
|
+
: cell(c.workflow.global, '**no soportado**', home);
|
|
103
|
+
lines.push(`| \`${a}\` | ${agentCell} | ${wfCell} | ${c.hooks ? `\`${c.hooks.type}\`` : '— (no tiene)'} ` +
|
|
104
|
+
`| ${injectionCell(c, home)} | ${c.minimumVersion ?? '— (sin gate)'} |`);
|
|
105
|
+
}
|
|
106
|
+
lines.push('');
|
|
107
|
+
lines.push('> Generado desde `cli/src/providers/index.ts`. **No editar a mano** — `npm run docs:matrix` lo regenera y');
|
|
108
|
+
lines.push('> `tests/structural/support-matrix-is-current.test.ts` falla si el documento y el código se separan.');
|
|
109
|
+
return lines.join('\n');
|
|
110
|
+
}
|
|
111
|
+
/** Reemplaza el bloque entre marcadores. Falla ruidosamente si faltan: un doc sin
|
|
112
|
+
* marcadores no se "arregla" agregando la tabla al final, se arregla avisando.
|
|
113
|
+
*
|
|
114
|
+
* Respeta el fin de linea del documento que recibe. Emitia LF siempre, asi que en un
|
|
115
|
+
* checkout de Windows —donde git entrega el .md con CRLF por defecto— regenerar producia
|
|
116
|
+
* un archivo de finales mezclados, y la comparacion del test fallaba por bytes que no
|
|
117
|
+
* tienen nada que ver con el contenido de la tabla. */
|
|
118
|
+
function spliceGenerated(markdown, generated) {
|
|
119
|
+
const begin = markdown.indexOf(exports.BEGIN_MARKER);
|
|
120
|
+
const end = markdown.indexOf(exports.END_MARKER);
|
|
121
|
+
if (begin === -1 || end === -1 || end < begin) {
|
|
122
|
+
throw new Error(`support-matrix.md no tiene los marcadores ${exports.BEGIN_MARKER} / ${exports.END_MARKER}`);
|
|
123
|
+
}
|
|
124
|
+
const eol = markdown.includes('\r\n') ? '\r\n' : '\n';
|
|
125
|
+
const block = generated.split('\n').join(eol);
|
|
126
|
+
return markdown.slice(0, begin + exports.BEGIN_MARKER.length)
|
|
127
|
+
+ eol + eol + block + eol + eol
|
|
128
|
+
+ markdown.slice(end);
|
|
129
|
+
}
|
|
130
|
+
exports.DOC_PATH = path_1.default.join(__dirname, '..', '..', 'docs', 'support-matrix.md');
|
|
131
|
+
/* istanbul ignore next — entrypoint de CLI, ejercitado por el test via las funciones puras */
|
|
132
|
+
if (require.main === module) {
|
|
133
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
134
|
+
const fs = require('fs');
|
|
135
|
+
const current = fs.readFileSync(exports.DOC_PATH, 'utf-8');
|
|
136
|
+
fs.writeFileSync(exports.DOC_PATH, spliceGenerated(current, renderProviderTables()), 'utf-8');
|
|
137
|
+
process.stdout.write(`support-matrix.md regenerado desde providers/index.ts\n`);
|
|
138
|
+
}
|
|
@@ -17,6 +17,7 @@ exports.planInitMutationTargets = planInitMutationTargets;
|
|
|
17
17
|
// entry when nothing exists there yet). The one failure mode that matters is
|
|
18
18
|
// under-enumeration: a real write to a path NOT in this list would land
|
|
19
19
|
// outside the backup session and survive a rollback.
|
|
20
|
+
const fs_1 = __importDefault(require("fs"));
|
|
20
21
|
const path_1 = __importDefault(require("path"));
|
|
21
22
|
const providers_1 = require("../../providers");
|
|
22
23
|
const bundles_1 = require("../bundles");
|
|
@@ -40,6 +41,17 @@ function physicalTarget(type, agent, scope, installName, projectRoot) {
|
|
|
40
41
|
return null;
|
|
41
42
|
}
|
|
42
43
|
}
|
|
44
|
+
/** La forma canonica de un directorio: resuelve symlinks si existe, y si no existe
|
|
45
|
+
* todavia devuelve la entrada tal cual — enumerar un destino aun inexistente es
|
|
46
|
+
* deliberado en este modulo (un backup vacio no es un problema; no enumerarlo si). */
|
|
47
|
+
function canonicalDir(dir) {
|
|
48
|
+
try {
|
|
49
|
+
return fs_1.default.realpathSync(dir);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return dir;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
43
55
|
function addBundleTargets(targets, bundleName, bundles, agent, scope, projectRoot, contentDir) {
|
|
44
56
|
let intents;
|
|
45
57
|
try {
|
|
@@ -72,7 +84,17 @@ function addBundleTargets(targets, bundleName, bundles, agent, scope, projectRoo
|
|
|
72
84
|
* read the project profile, but never writes).
|
|
73
85
|
*/
|
|
74
86
|
function planInitMutationTargets(params) {
|
|
75
|
-
const {
|
|
87
|
+
const { agent, bundles } = params;
|
|
88
|
+
// `cwd` canonico ANTES de derivar nada de el. Algunos destinos salen de `cwd` tal
|
|
89
|
+
// cual y otros de `findProjectRoot(cwd)`, que hace `realpathSync`: mientras las dos
|
|
90
|
+
// formas coincidan el `Set` deduplica y no se nota, pero en macOS `/var/folders/…`
|
|
91
|
+
// es un symlink a `/private/var/folders/…` y entonces NO coinciden — la CI de macOS
|
|
92
|
+
// mostro `AGENTS.md` y `.awm/context/awm-context.md` dos veces, una por forma.
|
|
93
|
+
//
|
|
94
|
+
// Esta lista es la que `beginBackupSession` respalda y la que el rollback restaura.
|
|
95
|
+
// Un archivo con dos entradas es un archivo respaldado dos veces y restaurado dos
|
|
96
|
+
// veces, en el mecanismo cuyo unico trabajo es dejar el disco como estaba.
|
|
97
|
+
const cwd = canonicalDir(params.cwd);
|
|
76
98
|
const targets = new Set();
|
|
77
99
|
// preferences.json + the artifact ownership ledger (state/artifacts.json)
|
|
78
100
|
targets.add(path_1.default.join((0, paths_1.awmHome)(), 'preferences.json'));
|
package/dist/src/index.js
CHANGED
|
@@ -88,9 +88,19 @@ function resolveSelectedArtifacts(selections) {
|
|
|
88
88
|
}
|
|
89
89
|
return Array.from(result.values());
|
|
90
90
|
}
|
|
91
|
+
// AWM instala BUNDLES, no artefactos sueltos. Es una decision de producto, no una
|
|
92
|
+
// limitacion pendiente: las skills de AWM se apoyan unas en otras (el spine de
|
|
93
|
+
// development-process invoca brainstorming, writing-plans, los gates de QA...), asi que
|
|
94
|
+
// una skill instalada sola casi nunca hace lo que el usuario espera. Ver
|
|
95
|
+
// docs/decisions.md, D-001.
|
|
96
|
+
//
|
|
97
|
+
// El flag `-t, --type` existia y `add.ts` NUNCA lo leia: se aceptaba y se descartaba en
|
|
98
|
+
// silencio. Peor, la doc y los playbooks de aceptacion documentaban `awm add <skill>
|
|
99
|
+
// --type skill` como el camino scripteado — una invocacion que siempre fallo con
|
|
100
|
+
// «Bundle "<skill>" not found». Los playbooks se escribieron contra la documentacion, no
|
|
101
|
+
// contra el comportamiento. Un flag que no se lee es peor que uno ausente: promete.
|
|
91
102
|
program.command('add [name]')
|
|
92
|
-
.description('Add a
|
|
93
|
-
.option('-t, --type <type>', 'Artifact type: skill, workflow, or process')
|
|
103
|
+
.description('Add a bundle (package of skills) interactively, or non-interactively with flags')
|
|
94
104
|
.option('-a, --agent <agent>', `Target agent: ${providers_1.AGENT_TARGETS.join(', ')}`)
|
|
95
105
|
.option('-s, --scope <scope>', 'Scope: local or global')
|
|
96
106
|
.option('-m, --method <method>', 'Install method: symlink or copy')
|
|
@@ -11,6 +11,17 @@ const exec_wrapper_1 = require("../../../src/commands/job/exec-wrapper");
|
|
|
11
11
|
const store_1 = require("../../../src/core/journal/store");
|
|
12
12
|
const paths_1 = require("../../../src/core/journal/paths");
|
|
13
13
|
const process_1 = require("../../../src/core/journal/process");
|
|
14
|
+
// Presupuesto explicito POR ENCIMA del global (30s en jest.config.js), como sus tres
|
|
15
|
+
// hermanos de `tests/commands/watch/`
|
|
16
|
+
// (integration 30s, supervisor-loop 60s, e2e-crash 180s). Este archivo era el unico de
|
|
17
|
+
// la familia sin uno, y hace el mismo trabajo que ellos: cada `collectAndReconcile`
|
|
18
|
+
// consulta si el proceso sigue vivo, y en Windows eso spawnea `tasklist` — cientos de ms
|
|
19
|
+
// por llamada bajo carga. El test de stall hace 3 iteraciones sobre dos refs (proceso +
|
|
20
|
+
// wrapper), o sea 6 consultas, y el default de 5s de jest quedaba justo en el borde:
|
|
21
|
+
// verde en dos corridas de Windows y timeout en la tercera, sin que cambiara el codigo.
|
|
22
|
+
// El sujeto del test es la semantica de deteccion de stall, no cuanto tarda; el reloj
|
|
23
|
+
// generoso no afloja ninguna asercion.
|
|
24
|
+
jest.setTimeout(60000);
|
|
14
25
|
const fakeSpawner = (job, nonce, logsRoot, repoRoot) => {
|
|
15
26
|
// Mismo contrato que el spawner real: dispara el wrapper y NO espera.
|
|
16
27
|
void (0, exec_wrapper_1.runExecWrapper)({ logsRoot, jobId: job.id, nonce, argv: job.argv, cwd: job.cwd, repoRoot }).catch(() => { });
|
|
@@ -31,11 +31,11 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
31
31
|
// purpose, and this step read that as "no project found" and wrote into cwd anyway —
|
|
32
32
|
// under the one flag whose entire promise is that it won't touch the project.
|
|
33
33
|
const fs_1 = __importDefault(require("fs"));
|
|
34
|
-
const os_1 = __importDefault(require("os"));
|
|
35
34
|
const path_1 = __importDefault(require("path"));
|
|
36
35
|
const steps_1 = require("../../../src/core/init/steps");
|
|
37
36
|
const mutation_targets_1 = require("../../../src/core/init/mutation-targets");
|
|
38
37
|
const materializer_1 = require("../../../src/core/context/materializer");
|
|
38
|
+
const tmp_1 = require("../../support/tmp");
|
|
39
39
|
function deps(agent, cwd, projectRoot, machineOnly = false) {
|
|
40
40
|
const installed = [];
|
|
41
41
|
const d = {
|
|
@@ -61,7 +61,7 @@ function deps(agent, cwd, projectRoot, machineOnly = false) {
|
|
|
61
61
|
}
|
|
62
62
|
describe('project-scope context injection without a project', () => {
|
|
63
63
|
let cwd;
|
|
64
|
-
beforeEach(() => { cwd =
|
|
64
|
+
beforeEach(() => { cwd = (0, tmp_1.mkCanonicalTmpDir)('awm-noproj-'); });
|
|
65
65
|
afterEach(() => { fs_1.default.rmSync(cwd, { recursive: true, force: true }); });
|
|
66
66
|
it.each(['copilot', 'cursor'])('%s: writes nothing at all under --machine-only', (agent) => {
|
|
67
67
|
const { d, installed } = deps(agent, cwd, null, true);
|
|
@@ -103,7 +103,7 @@ describe('project-scope context injection without a project', () => {
|
|
|
103
103
|
});
|
|
104
104
|
describe('mutation-targets covers the local-scope context paths independently', () => {
|
|
105
105
|
let cwd;
|
|
106
|
-
beforeEach(() => { cwd =
|
|
106
|
+
beforeEach(() => { cwd = (0, tmp_1.mkCanonicalTmpDir)('awm-noproj-mt-'); });
|
|
107
107
|
afterEach(() => { fs_1.default.rmSync(cwd, { recursive: true, force: true }); });
|
|
108
108
|
it.each(['copilot', 'cursor'])('%s: enumerates AGENTS.md and .awm/context even with no project root', (agent) => {
|
|
109
109
|
const targets = (0, mutation_targets_1.planInitMutationTargets)({ cwd, agent, bundles: [] });
|
|
@@ -0,0 +1,81 @@
|
|
|
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
|
+
// `planInitMutationTargets` mezclaba dos formas de la misma ruta.
|
|
7
|
+
//
|
|
8
|
+
// Deriva algunos destinos de `cwd` tal como lo recibe, y otros de
|
|
9
|
+
// `findProjectRoot(cwd)` — que hace `realpathSync`. Mientras las dos coincidan, el `Set`
|
|
10
|
+
// deduplica y nadie lo nota. En macOS NO coinciden: `/var/folders/…` es un symlink a
|
|
11
|
+
// `/private/var/folders/…`, asi que la CI de macOS mostro `AGENTS.md` y
|
|
12
|
+
// `.awm/context/awm-context.md` DOS VECES en la lista de destinos, una por cada forma:
|
|
13
|
+
//
|
|
14
|
+
// /var/folders/…/AGENTS.md ← derivado de cwd
|
|
15
|
+
// /private/var/folders/…/AGENTS.md ← derivado de findProjectRoot(cwd)
|
|
16
|
+
//
|
|
17
|
+
// Esa lista es lo que `beginBackupSession` respalda y lo que el rollback restaura. Un
|
|
18
|
+
// mismo archivo entrando dos veces significa dos entradas de backup para un solo
|
|
19
|
+
// archivo, y un rollback que lo restaura dos veces — sobre un mecanismo cuyo unico
|
|
20
|
+
// trabajo es dejar el disco exactamente como estaba.
|
|
21
|
+
//
|
|
22
|
+
// El bug no necesita macOS: cualquier `cwd` que pase por un symlink lo reproduce. Este
|
|
23
|
+
// test arma esa situacion a mano, asi que corre igual en los tres sistemas.
|
|
24
|
+
const fs_1 = __importDefault(require("fs"));
|
|
25
|
+
const os_1 = __importDefault(require("os"));
|
|
26
|
+
const path_1 = __importDefault(require("path"));
|
|
27
|
+
const mutation_targets_1 = require("../../../src/core/init/mutation-targets");
|
|
28
|
+
describe('los destinos de mutacion son canonicos: un archivo, una entrada', () => {
|
|
29
|
+
let real;
|
|
30
|
+
let linkDir;
|
|
31
|
+
beforeEach(() => {
|
|
32
|
+
// `real` es el directorio de verdad; `linkDir` es un symlink que apunta ahi.
|
|
33
|
+
// Pasar `linkDir` como cwd es exactamente lo que hace macOS con /var → /private/var.
|
|
34
|
+
const base = fs_1.default.realpathSync(fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-canon-')));
|
|
35
|
+
real = path_1.default.join(base, 'real');
|
|
36
|
+
linkDir = path_1.default.join(base, 'link');
|
|
37
|
+
fs_1.default.mkdirSync(path_1.default.join(real, '.git'), { recursive: true }); // marcador de proyecto
|
|
38
|
+
fs_1.default.symlinkSync(real, linkDir, 'dir');
|
|
39
|
+
});
|
|
40
|
+
afterEach(() => {
|
|
41
|
+
fs_1.default.rmSync(path_1.default.dirname(real), { recursive: true, force: true });
|
|
42
|
+
});
|
|
43
|
+
/** Todo destino resuelto a su forma canonica: dos entradas que resuelven al mismo
|
|
44
|
+
* archivo son la misma entrada, sin importar como se escribieron. */
|
|
45
|
+
const canonical = (p) => {
|
|
46
|
+
try {
|
|
47
|
+
return fs_1.default.realpathSync(p);
|
|
48
|
+
}
|
|
49
|
+
catch { /* aun no existe */ }
|
|
50
|
+
const parent = path_1.default.dirname(p);
|
|
51
|
+
try {
|
|
52
|
+
return path_1.default.join(fs_1.default.realpathSync(parent), path_1.default.basename(p));
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return p;
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
it.each(['copilot', 'cursor'])('%s: no enumera el mismo archivo dos veces cuando cwd pasa por un symlink', (agent) => {
|
|
59
|
+
const targets = (0, mutation_targets_1.planInitMutationTargets)({ cwd: linkDir, agent, bundles: [] });
|
|
60
|
+
const seen = new Map();
|
|
61
|
+
for (const t of targets) {
|
|
62
|
+
const key = canonical(t);
|
|
63
|
+
seen.set(key, [...(seen.get(key) ?? []), t]);
|
|
64
|
+
}
|
|
65
|
+
const duplicados = [...seen.entries()].filter(([, forms]) => forms.length > 1);
|
|
66
|
+
expect(duplicados).toEqual([]);
|
|
67
|
+
});
|
|
68
|
+
it('sigue enumerando AGENTS.md y el contexto materializado — deduplicar no es perder', () => {
|
|
69
|
+
// La correccion es colapsar formas equivalentes, NO dejar de cubrir el archivo:
|
|
70
|
+
// sub-enumerar es el unico modo de falla que derrota el rollback en silencio.
|
|
71
|
+
const targets = (0, mutation_targets_1.planInitMutationTargets)({ cwd: linkDir, agent: 'copilot', bundles: [] })
|
|
72
|
+
.map(canonical);
|
|
73
|
+
expect(targets).toContain(path_1.default.join(real, 'AGENTS.md'));
|
|
74
|
+
expect(targets).toContain(path_1.default.join(real, '.awm', 'context', 'awm-context.md'));
|
|
75
|
+
});
|
|
76
|
+
it('no depende del symlink: con un cwd ya canonico da el mismo resultado', () => {
|
|
77
|
+
const viaLink = (0, mutation_targets_1.planInitMutationTargets)({ cwd: linkDir, agent: 'copilot', bundles: [] }).map(canonical).sort();
|
|
78
|
+
const viaReal = (0, mutation_targets_1.planInitMutationTargets)({ cwd: real, agent: 'copilot', bundles: [] }).map(canonical).sort();
|
|
79
|
+
expect(viaLink).toEqual(viaReal);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
@@ -17,9 +17,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
17
17
|
// tests/core/skill-integrity.test.ts. Per CLAUDE.md, no test may touch the
|
|
18
18
|
// real ~/.awm.
|
|
19
19
|
const fs_1 = __importDefault(require("fs"));
|
|
20
|
-
const os_1 = __importDefault(require("os"));
|
|
21
20
|
const path_1 = __importDefault(require("path"));
|
|
22
21
|
const materializer_1 = require("../../../src/core/context/materializer");
|
|
22
|
+
const tmp_1 = require("../../support/tmp");
|
|
23
23
|
function bundle(name, scope, skills) {
|
|
24
24
|
return {
|
|
25
25
|
name, description: '', version: '1.0.0', scope, visibility: 'public',
|
|
@@ -32,7 +32,7 @@ let originalHome;
|
|
|
32
32
|
let originalAwmHome;
|
|
33
33
|
let extraDirs;
|
|
34
34
|
beforeEach(() => {
|
|
35
|
-
tmpHome =
|
|
35
|
+
tmpHome = (0, tmp_1.mkCanonicalTmpDir)('awm-mutation-targets-');
|
|
36
36
|
originalHome = process.env.HOME;
|
|
37
37
|
originalAwmHome = process.env.AWM_HOME;
|
|
38
38
|
process.env.HOME = tmpHome;
|
|
@@ -67,7 +67,7 @@ function load() {
|
|
|
67
67
|
}
|
|
68
68
|
/** A cwd guaranteed to have no project-root marker (.git/package.json/.awm) in its ancestry. */
|
|
69
69
|
function bareCwd() {
|
|
70
|
-
const dir =
|
|
70
|
+
const dir = (0, tmp_1.mkCanonicalTmpDir)('awm-mutation-targets-cwd-');
|
|
71
71
|
extraDirs.push(dir);
|
|
72
72
|
return dir;
|
|
73
73
|
}
|
|
@@ -158,7 +158,7 @@ describe('planInitMutationTargets', () => {
|
|
|
158
158
|
});
|
|
159
159
|
describe('project-level targets', () => {
|
|
160
160
|
function makeProjectRoot() {
|
|
161
|
-
const root =
|
|
161
|
+
const root = (0, tmp_1.mkCanonicalTmpDir)('awm-mutation-targets-project-');
|
|
162
162
|
fs_1.default.mkdirSync(path_1.default.join(root, '.git')); // project-root marker for findProjectRoot
|
|
163
163
|
extraDirs.push(root);
|
|
164
164
|
return root;
|
|
@@ -22,17 +22,17 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
22
22
|
// For Copilot the gap is total, not partial: `skill.global` is null, so *every* skill
|
|
23
23
|
// it has is project-scope, and none of them had any integrity path whatsoever.
|
|
24
24
|
const fs_1 = __importDefault(require("fs"));
|
|
25
|
-
const os_1 = __importDefault(require("os"));
|
|
26
25
|
const path_1 = __importDefault(require("path"));
|
|
27
26
|
const skill_integrity_1 = require("../../src/core/skill-integrity");
|
|
28
27
|
const providers_1 = require("../../src/providers");
|
|
28
|
+
const tmp_1 = require("../support/tmp");
|
|
29
29
|
describe('project-scope skill links get the same integrity path as global ones', () => {
|
|
30
30
|
let registry;
|
|
31
31
|
let projectRoot;
|
|
32
32
|
const made = [];
|
|
33
33
|
beforeEach(() => {
|
|
34
|
-
registry =
|
|
35
|
-
projectRoot =
|
|
34
|
+
registry = (0, tmp_1.mkCanonicalTmpDir)('awm-projlink-reg-');
|
|
35
|
+
projectRoot = (0, tmp_1.mkCanonicalTmpDir)('awm-projlink-proj-');
|
|
36
36
|
made.push(registry, projectRoot);
|
|
37
37
|
fs_1.default.mkdirSync(path_1.default.join(registry, 'skills', 'alive'), { recursive: true });
|
|
38
38
|
fs_1.default.writeFileSync(path_1.default.join(registry, 'skills', 'alive', 'SKILL.md'), '# alive\n');
|
|
@@ -104,7 +104,7 @@ describe('awm sync reaches the project-scope reconciliation', () => {
|
|
|
104
104
|
let saved;
|
|
105
105
|
beforeEach(() => {
|
|
106
106
|
jest.resetModules();
|
|
107
|
-
home =
|
|
107
|
+
home = (0, tmp_1.mkCanonicalTmpDir)('awm-syncwire-home-');
|
|
108
108
|
saved = { HOME: process.env.HOME, AWM_HOME: process.env.AWM_HOME };
|
|
109
109
|
process.env.HOME = home;
|
|
110
110
|
process.env.AWM_HOME = path_1.default.join(home, '.awm');
|
|
@@ -113,7 +113,7 @@ describe('awm sync reaches the project-scope reconciliation', () => {
|
|
|
113
113
|
enabledAgents: ['claude-code'], defaultAgent: 'claude-code',
|
|
114
114
|
installMethod: 'symlink', defaultScope: 'global',
|
|
115
115
|
}));
|
|
116
|
-
projectRoot =
|
|
116
|
+
projectRoot = (0, tmp_1.mkCanonicalTmpDir)('awm-syncwire-proj-');
|
|
117
117
|
fs_1.default.mkdirSync(path_1.default.join(projectRoot, '.awm'), { recursive: true });
|
|
118
118
|
});
|
|
119
119
|
afterEach(() => {
|
|
@@ -0,0 +1,100 @@
|
|
|
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
|
+
// Guarda estructural: `docs/support-matrix.md` no puede desalinearse de
|
|
7
|
+
// `src/providers/index.ts`.
|
|
8
|
+
//
|
|
9
|
+
// La tabla escrita a mano ya mintio. Decia que Antigravity instalaba en
|
|
10
|
+
// `~/.agents/skills` y `.agents/skills`; el codigo dice `~/.gemini/antigravity/skills` y
|
|
11
|
+
// `.agent/skills` — singular — y ademas es el unico provider con `global_workflows`, cosa
|
|
12
|
+
// que la tabla ni mencionaba. Nadie lo noto porque nada obligaba a mirarlo: la tabla se
|
|
13
|
+
// escribio una vez, el codigo siguio cambiando, y el documento se cita en presentaciones y
|
|
14
|
+
// se planifica encima.
|
|
15
|
+
//
|
|
16
|
+
// Este test hace que ese desvio sea imposible de mergear. No verifica que la tabla sea
|
|
17
|
+
// "linda": verifica que sea LA MISMA que produce el codigo hoy.
|
|
18
|
+
const fs_1 = __importDefault(require("fs"));
|
|
19
|
+
const support_matrix_1 = require("../../scripts/support-matrix");
|
|
20
|
+
describe('docs/support-matrix.md refleja el codigo', () => {
|
|
21
|
+
it('el bloque generado esta al dia', () => {
|
|
22
|
+
const doc = fs_1.default.readFileSync(support_matrix_1.DOC_PATH, 'utf-8');
|
|
23
|
+
const expected = (0, support_matrix_1.spliceGenerated)(doc, (0, support_matrix_1.renderProviderTables)());
|
|
24
|
+
// Mensaje accionable: el que rompe esto casi siempre acaba de tocar
|
|
25
|
+
// providers/index.ts y no sabe que este documento existe.
|
|
26
|
+
expect(doc).toBe(expected);
|
|
27
|
+
});
|
|
28
|
+
it('el documento conserva sus marcadores', () => {
|
|
29
|
+
const doc = fs_1.default.readFileSync(support_matrix_1.DOC_PATH, 'utf-8');
|
|
30
|
+
expect(doc).toContain(support_matrix_1.BEGIN_MARKER);
|
|
31
|
+
expect(doc).toContain(support_matrix_1.END_MARKER);
|
|
32
|
+
});
|
|
33
|
+
it('la tabla nombra a los seis providers declarados', () => {
|
|
34
|
+
// Que el bloque este "al dia" no sirve si el generador se olvidara de un provider:
|
|
35
|
+
// doc y generador coincidirian, los dos incompletos.
|
|
36
|
+
const { AGENT_TARGETS } = require('../../src/providers');
|
|
37
|
+
const generated = (0, support_matrix_1.renderProviderTables)();
|
|
38
|
+
for (const agent of AGENT_TARGETS) {
|
|
39
|
+
expect(generated).toContain(`\`${agent}\``);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
it('abrevia el home y usa separadores POSIX, para no depender de la maquina', () => {
|
|
43
|
+
// Con un HOME inventado: si la tabla dependiera de la maquina que la genera, este
|
|
44
|
+
// home apareceria literal en la salida y el doc comiteado cambiaria segun quien
|
|
45
|
+
// corriera el generador — el test de arriba se volveria ruido permanente.
|
|
46
|
+
const saved = process.env.HOME;
|
|
47
|
+
process.env.HOME = '/home/inventado';
|
|
48
|
+
try {
|
|
49
|
+
jest.resetModules();
|
|
50
|
+
const { renderProviderTables: fresh } = require('../../scripts/support-matrix');
|
|
51
|
+
const generated = fresh();
|
|
52
|
+
expect(generated).toContain('`~/.claude/skills`');
|
|
53
|
+
expect(generated).not.toContain('/home/inventado');
|
|
54
|
+
expect(generated).not.toContain('\\');
|
|
55
|
+
}
|
|
56
|
+
finally {
|
|
57
|
+
if (saved === undefined)
|
|
58
|
+
delete process.env.HOME;
|
|
59
|
+
else
|
|
60
|
+
process.env.HOME = saved;
|
|
61
|
+
jest.resetModules();
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
// La abreviacion a `~` se probaba solo end-to-end, y en Linux eso no distingue entre
|
|
65
|
+
// "normaliza bien" y "los separadores ya coincidian". La CI de Windows encontro que no
|
|
66
|
+
// normalizaba: comparaba el prefijo ANTES de convertir separadores, asi que alla no
|
|
67
|
+
// abreviaba nada y la tabla regenerada no era la comiteada. Probar la unidad con las
|
|
68
|
+
// dos formas de ruta lo detecta en cualquier sistema, sin fingir la plataforma.
|
|
69
|
+
describe('homeRelative normaliza antes de comparar', () => {
|
|
70
|
+
it.each([
|
|
71
|
+
['posix', '/home/x', '/home/x/.claude/skills', '~/.claude/skills'],
|
|
72
|
+
['win32', 'C:\\Users\\x', 'C:\\Users\\x\\.claude\\skills', '~/.claude/skills'],
|
|
73
|
+
['mixto', '/home/x', '\\home\\x\\.claude\\skills', '~/.claude/skills'],
|
|
74
|
+
['sin prefijo', '/home/x', '/opt/otro/skills', '/opt/otro/skills'],
|
|
75
|
+
])('%s', (_n, home, input, expected) => {
|
|
76
|
+
expect((0, support_matrix_1.homeRelative)(input, home)).toBe(expected);
|
|
77
|
+
});
|
|
78
|
+
it('nunca deja un separador de Windows en la salida', () => {
|
|
79
|
+
expect((0, support_matrix_1.homeRelative)('C:\\Users\\x\\.codex\\agents', 'C:\\Users\\x')).not.toContain('\\');
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
it('respeta el fin de linea del documento (un checkout de Windows entrega CRLF)', () => {
|
|
83
|
+
// Sin esto, regenerar en Windows dejaba el archivo con finales mezclados y el test
|
|
84
|
+
// de arriba fallaba por bytes ajenos al contenido. Es el mismo error de fondo que
|
|
85
|
+
// el de los separadores: asumir la forma de POSIX para un dato que la plataforma
|
|
86
|
+
// decide.
|
|
87
|
+
const crlfDoc = `intro\r\n${support_matrix_1.BEGIN_MARKER}\r\nviejo\r\n${support_matrix_1.END_MARKER}\r\nfin\r\n`;
|
|
88
|
+
const out = (0, support_matrix_1.spliceGenerated)(crlfDoc, 'linea uno\nlinea dos');
|
|
89
|
+
expect(out).toContain('linea uno\r\nlinea dos');
|
|
90
|
+
expect(out.split('\r\n').length - 1).toBe(out.split('\n').length - 1); // ni un LF suelto
|
|
91
|
+
});
|
|
92
|
+
it('marca como no soportado, no como ausente, el scope que un provider no tiene', () => {
|
|
93
|
+
// La diferencia entre "no soportado" y una celda vacia es exactamente lo que el
|
|
94
|
+
// documento existe para no dejar ambiguo: Copilot no tiene scope global por
|
|
95
|
+
// decision del producto que integramos, no porque falte implementarlo.
|
|
96
|
+
const generated = (0, support_matrix_1.renderProviderTables)();
|
|
97
|
+
const copilotRow = generated.split('\n').find((l) => l.startsWith('| `copilot` |'));
|
|
98
|
+
expect(copilotRow).toContain('**no soportado**');
|
|
99
|
+
});
|
|
100
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.mkCanonicalTmpDir = mkCanonicalTmpDir;
|
|
7
|
+
// Directorios temporales en forma canonica, para tests que comparan rutas.
|
|
8
|
+
//
|
|
9
|
+
// En macOS `os.tmpdir()` devuelve `/var/folders/…`, que es un SYMLINK a
|
|
10
|
+
// `/private/var/folders/…`. El producto resuelve el root del proyecto con `realpathSync`
|
|
11
|
+
// (`findProjectRoot`, `core/profile.ts`), asi que devuelve la forma canonica. Un test que
|
|
12
|
+
// arma su expectativa con el `mkdtemp` crudo compara `/var/…` contra `/private/var/…` y
|
|
13
|
+
// falla sobre un producto que se comporta bien. En Linux y Windows las dos formas
|
|
14
|
+
// coinciden, asi que el problema es invisible hasta que corre en macOS — paso tres veces
|
|
15
|
+
// en la primera corrida de macOS en CI.
|
|
16
|
+
//
|
|
17
|
+
// Usar esto en cualquier test que:
|
|
18
|
+
// - compare una ruta contra la que devuelve el producto, o
|
|
19
|
+
// - le pase el tmpdir al producto y despues afirme sobre lo que salio.
|
|
20
|
+
//
|
|
21
|
+
// Un test que solo escribe y lee archivos bajo su tmpdir no lo necesita: las dos formas
|
|
22
|
+
// llegan al mismo inodo. Por eso esto es un helper y no una migracion masiva — envolver
|
|
23
|
+
// los 113 archivos que crean tmpdirs seria ruido, no rigor.
|
|
24
|
+
const fs_1 = __importDefault(require("fs"));
|
|
25
|
+
const os_1 = __importDefault(require("os"));
|
|
26
|
+
const path_1 = __importDefault(require("path"));
|
|
27
|
+
/** `fs.mkdtempSync` + `realpathSync`: el mismo directorio, en la forma que el producto
|
|
28
|
+
* va a usar. El `realpathSync` no puede fallar — el directorio acaba de crearse. */
|
|
29
|
+
function mkCanonicalTmpDir(prefix) {
|
|
30
|
+
return fs_1.default.realpathSync(fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), prefix)));
|
|
31
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentic-workflow-manager",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.0",
|
|
4
4
|
"main": "dist/src/index.js",
|
|
5
5
|
"bin": {
|
|
6
6
|
"awm": "./dist/src/index.js"
|
|
@@ -23,7 +23,8 @@
|
|
|
23
23
|
"prerelease": "npm run build",
|
|
24
24
|
"release": "node dist/src/release/index.js",
|
|
25
25
|
"prepack": "npm run build",
|
|
26
|
-
"prepublishOnly": "npm run build"
|
|
26
|
+
"prepublishOnly": "npm run build",
|
|
27
|
+
"docs:matrix": "ts-node scripts/support-matrix.ts"
|
|
27
28
|
},
|
|
28
29
|
"keywords": [
|
|
29
30
|
"agentic",
|