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.
@@ -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 { cwd, agent, bundles } = params;
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 skill, workflow, or process interactively (or non-interactively with flags)')
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 = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-noproj-')); });
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 = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-noproj-mt-')); });
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 = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-mutation-targets-'));
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 = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-mutation-targets-cwd-'));
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 = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-mutation-targets-project-'));
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 = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-projlink-reg-'));
35
- projectRoot = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-projlink-proj-'));
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 = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-syncwire-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 = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-syncwire-proj-'));
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.13.6",
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",