agentic-workflow-manager 6.1.2 → 6.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -67,7 +67,7 @@ function binaryVersionCheck(agent) {
67
67
  * verificacion que no ocurria. Ahora tambien comprueba contenido, con el marcador que
68
68
  * declara el renderer (`rendererIntegrityMarker`): un archivo con la extension correcta
69
69
  * y el cuerpo vacio o truncado ya no pasa como sano. */
70
- function skillsGlobalCheck(dir, owners, integrity, renderer) {
70
+ function skillsGlobalCheck(dir, owners, integrity, renderer, artifacts) {
71
71
  if (dir === null)
72
72
  return null;
73
73
  if (renderer !== 'link') {
@@ -107,18 +107,37 @@ function skillsGlobalCheck(dir, owners, integrity, renderer) {
107
107
  // Comprobar presencia y extension dejaba pasar un archivo correcto por fuera y
108
108
  // vacio o truncado por dentro — el agente cargaba nada y doctor decia que si.
109
109
  const corrupt = corruptRendered(dir, rendered, renderer);
110
+ if (corrupt.length > 0) {
111
+ return {
112
+ id: 'skills.global', state: 'broken', target: dir,
113
+ owners: owners.length > 1 ? owners : undefined,
114
+ detail: `${corrupt.length} rendered file(s) missing their expected content (${corrupt.slice(0, 3).join(', ')})`,
115
+ // Codigo propio: no es una usurpacion (nadie lo reemplazo por otra cosa) ni
116
+ // un symlink colgante. Es nuestro archivo, con el contenido incompleto — lo
117
+ // arregla re-instalar el bundle, que lo reescribe.
118
+ remediationCode: 'reinstall-rendered-artifacts',
119
+ };
120
+ }
121
+ // Frescura, DESPUES de integridad: un archivo truncado tambien difiere de su
122
+ // fuente, y "incompleto" es mas util que "viejo". Solo aplica a renderizados — un
123
+ // symlink apunta al registry, asi que `awm update` lo actualiza solo. Los
124
+ // generados NO: quedan con el contenido de la version anterior hasta que alguien
125
+ // corra `awm sync`, y hasta ahora nada lo decia.
126
+ const stale = staleRendered(dir, artifacts);
110
127
  return {
111
128
  id: 'skills.global',
112
- state: corrupt.length > 0 ? 'broken' : 'supported',
129
+ state: stale.length > 0 ? 'stale' : 'supported',
113
130
  target: dir,
114
131
  owners: owners.length > 1 ? owners : undefined,
115
- detail: corrupt.length > 0
116
- ? `${corrupt.length} rendered file(s) missing their expected content (${corrupt.slice(0, 3).join(', ')})`
132
+ detail: stale.length > 0
133
+ ? `${stale.length} rendered file(s) no longer match the installed registry (${stale.slice(0, 3).join(', ')})`
117
134
  : undefined,
118
- // Codigo propio: no es una usurpacion (nadie lo reemplazo por otra cosa) ni un
119
- // symlink colgante. Es nuestro archivo, con el contenido incompleto lo
120
- // arregla re-instalar el bundle, que lo reescribe.
121
- remediationCode: corrupt.length > 0 ? 'reinstall-rendered-artifacts' : undefined,
135
+ // `reinstall-bundle`, y NO `awm-sync` ni `awm-init`: se midio cual de los
136
+ // cuatro comandos refresca de verdad un renderizado viejo, y solo `awm add`
137
+ // lo hace. `init` y `sync` son idempotentes por presencia —ven el archivo y
138
+ // no lo tocan— y `update` refresca el registry, no lo derivado de el. Ofrecer
139
+ // un remedio que corre limpio sin cambiar nada es peor que no ofrecer ninguno.
140
+ remediationCode: stale.length > 0 ? 'reinstall-bundle' : undefined,
122
141
  };
123
142
  }
124
143
  const shared = owners.length > 1;
@@ -178,6 +197,25 @@ function skillsGlobalCheck(dir, owners, integrity, renderer) {
178
197
  * Un archivo ilegible cuenta como corrupto: no poder leerlo no es evidencia de que este
179
198
  * bien. Es la misma disciplina que el resto de los checks — nunca verde por no mirar.
180
199
  */
200
+ function staleRendered(dir, records) {
201
+ return records
202
+ .filter((r) => r.renderer !== 'link' && path_1.default.dirname(r.targetPath) === dir)
203
+ .filter((r) => {
204
+ try {
205
+ const expected = (0, registry_1.renderArtifact)(r.renderer, r.sourcePath);
206
+ if (expected === null)
207
+ return false;
208
+ return fs_1.default.readFileSync(r.targetPath, 'utf8') !== expected;
209
+ }
210
+ catch {
211
+ // Fuente ausente (el registry ya no la trae) o target ilegible: no es
212
+ // desactualizacion, es otra cosa, y la reportan los checks que
213
+ // corresponden. Aca "no puedo comparar" no se convierte en "esta viejo".
214
+ return false;
215
+ }
216
+ })
217
+ .map((r) => path_1.default.basename(r.targetPath));
218
+ }
181
219
  function corruptRendered(dir, files, renderer) {
182
220
  const marker = (0, registry_1.rendererIntegrityMarker)(renderer);
183
221
  if (marker === null)
@@ -411,6 +449,10 @@ function gatherProviderChecks(agents, scanSkills, projectRoot) {
411
449
  for (const dir of ownersByDir.keys()) {
412
450
  scansByDir.set(dir, scanSkills(dir));
413
451
  }
452
+ // El ledger se lee UNA vez por corrida y se comparte: es la unica forma de saber
453
+ // que fuente produjo cada artefacto renderizado, y por lo tanto de preguntar si
454
+ // sigue coincidiendo con ella.
455
+ const artifacts = safeArtifactState();
414
456
  return agents.map((agent) => {
415
457
  const provider = (0, providers_1.providerFor)(agent);
416
458
  const dir = provider.skill.global;
@@ -419,7 +461,7 @@ function gatherProviderChecks(agents, scanSkills, projectRoot) {
419
461
  ?? { valid: [], repairable: [], dead: [], usurped: [] };
420
462
  const checks = [
421
463
  binaryVersionCheck(agent),
422
- skillsGlobalCheck(dir, owners, integrity, provider.skill.renderer),
464
+ skillsGlobalCheck(dir, owners, integrity, provider.skill.renderer, artifacts),
423
465
  agentsNativeCheck(agent),
424
466
  workflowsGlobalCheck(agent),
425
467
  hookTrustCheck(agent),
@@ -30,9 +30,7 @@ const crypto_1 = __importDefault(require("crypto"));
30
30
  const artifact_state_1 = require("./artifact-state");
31
31
  const paths_1 = require("./paths");
32
32
  const atomic_file_1 = require("./atomic-file");
33
- const codex_agent_1 = require("./renderers/codex-agent");
34
- const cursor_mdc_1 = require("./renderers/cursor-mdc");
35
- const copilot_instructions_1 = require("./renderers/copilot-instructions");
33
+ const registry_1 = require("./renderers/registry");
36
34
  const executor_1 = require("./executor");
37
35
  /**
38
36
  * The single timestamp-sanitization rule for transaction IDs, shared by
@@ -249,9 +247,6 @@ function stageRenderedFile(content, targetPath) {
249
247
  * are sourced from that directory's SKILL.md, so every call site needs this
250
248
  * same one-line join instead of reading `op.sourcePath` directly.
251
249
  */
252
- function readSkillMdSource(op) {
253
- return fs_1.default.readFileSync(path_1.default.join(op.sourcePath, 'SKILL.md'), 'utf8');
254
- }
255
250
  /**
256
251
  * The real, filesystem-touching TransactionDeps used by applyInstallPlan by
257
252
  * default. Renders `codex-agent-toml`/`cursor-mdc`/`copilot-instructions`
@@ -270,15 +265,10 @@ function defaultTransactionDeps() {
270
265
  }
271
266
  // Renders without writing anything, purely to surface parse errors
272
267
  // before any backup/replace happens.
273
- if (op.renderer === 'codex-agent-toml') {
274
- (0, codex_agent_1.renderCodexAgent)(fs_1.default.readFileSync(op.sourcePath, 'utf8'));
275
- }
276
- else if (op.renderer === 'cursor-mdc') {
277
- (0, cursor_mdc_1.renderCursorMdc)(readSkillMdSource(op));
278
- }
279
- else if (op.renderer === 'copilot-instructions') {
280
- (0, copilot_instructions_1.renderCopilotInstructions)(readSkillMdSource(op));
281
- }
268
+ // Renderiza y descarta: el objetivo es que un error de parseo salte ANTES
269
+ // de tocar nada. El dispatch sale de la tabla (`renderArtifact`), no de una
270
+ // copia local — esta era una de las dos que habia en este archivo.
271
+ (0, registry_1.renderArtifact)(op.renderer, op.sourcePath);
282
272
  },
283
273
  backup(op, backupDir) {
284
274
  if (createdAt === null)
@@ -295,19 +285,11 @@ function defaultTransactionDeps() {
295
285
  return entry.existed ? path_1.default.join(backupDir, entry.backupRelPath) : null;
296
286
  },
297
287
  stage(op) {
298
- if (op.renderer === 'codex-agent-toml') {
299
- const rendered = (0, codex_agent_1.renderCodexAgent)(fs_1.default.readFileSync(op.sourcePath, 'utf8'));
300
- return stageRenderedFile(rendered, op.targetPath);
301
- }
302
- if (op.renderer === 'cursor-mdc') {
303
- const rendered = (0, cursor_mdc_1.renderCursorMdc)(readSkillMdSource(op));
304
- return stageRenderedFile(rendered, op.targetPath);
305
- }
306
- if (op.renderer === 'copilot-instructions') {
307
- const rendered = (0, copilot_instructions_1.renderCopilotInstructions)(readSkillMdSource(op));
308
- return stageRenderedFile(rendered, op.targetPath);
309
- }
310
- return (0, executor_1.stageArtifact)(op.sourcePath, op.targetPath, op.method);
288
+ const rendered = (0, registry_1.renderArtifact)(op.renderer, op.sourcePath);
289
+ // `null` = renderer `link`: no genera contenido, se instala el artefacto tal cual.
290
+ return rendered === null
291
+ ? (0, executor_1.stageArtifact)(op.sourcePath, op.targetPath, op.method)
292
+ : stageRenderedFile(rendered, op.targetPath);
311
293
  },
312
294
  replace(op, staged) {
313
295
  (0, executor_1.replaceArtifact)(staged, op.targetPath);
@@ -1,8 +1,40 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  exports.renderedFilename = renderedFilename;
4
7
  exports.rendererExtension = rendererExtension;
5
8
  exports.rendererIntegrityMarker = rendererIntegrityMarker;
9
+ exports.renderArtifact = renderArtifact;
10
+ // src/core/renderers/registry.ts
11
+ //
12
+ // Modulo HOJA: la UNICA fuente de verdad sobre "que archivo produce cada
13
+ // renderer en disco".
14
+ //
15
+ // Por que existe. Ese hecho estaba escrito a mano en TRES lugares divergentes
16
+ // — `physicalTarget` (install-planner.ts, el escritor), `RENDERED_SKILL_EXTENSIONS`
17
+ // (diagnostics/provider-checks.ts, que ademas omitia `codex-agent-toml`), y un
18
+ // ternario suelto para agentes en diagnostics/context.ts — y estaba AUSENTE en
19
+ // el cuarto, `classifyLinks`, que es justamente el que decide si un artefacto
20
+ // "esta instalado".
21
+ //
22
+ // La consecuencia no fue teorica: el escritor dejaba `using-awm.mdc` en disco y
23
+ // el lector buscaba `using-awm`, asi que para Cursor y Copilot
24
+ // - `awm init` NUNCA era idempotente (reinstalaba el baseline entero en cada
25
+ // corrida, dejando 2 directorios de backup sin podar por corrida), y
26
+ // - `awm doctor` mostraba un rojo permanente cuyo remedio no podia satisfacerlo.
27
+ // Lo mismo para los artefactos `agent` de claude-code, el provider por defecto.
28
+ //
29
+ // Regla: NINGUN modulo vuelve a derivar una extension a mano. Si aparece un
30
+ // renderer nuevo, se agrega aca y todos los consumidores quedan correctos por
31
+ // construccion — que es lo que convierte "agregar un provider" en un cambio
32
+ // localizado en vez de una caceria por seis archivos.
33
+ const fs_1 = __importDefault(require("fs"));
34
+ const path_1 = __importDefault(require("path"));
35
+ const codex_agent_1 = require("./codex-agent");
36
+ const cursor_mdc_1 = require("./cursor-mdc");
37
+ const copilot_instructions_1 = require("./copilot-instructions");
6
38
  /** Extension que el renderer estampa sobre el nombre base, o `null` cuando el
7
39
  * artefacto se instala con el nombre tal cual (renderer `link`). */
8
40
  const RENDERER_EXTENSION = {
@@ -61,3 +93,35 @@ function rendererExtension(renderer) {
61
93
  function rendererIntegrityMarker(renderer) {
62
94
  return RENDERER_INTEGRITY_MARKER[renderer];
63
95
  }
96
+ /**
97
+ * Contenido que `renderer` produce AHORA a partir de `sourcePath`, o `null` para `link`
98
+ * (no genera nada: instala el artefacto tal cual).
99
+ *
100
+ * Es la tercera cosa que un renderer tiene que declarar, junto con su extension y su
101
+ * marcador — y por eso vive en esta tabla. El dispatch `renderer id → funcion` estaba
102
+ * escrito DOS veces dentro de `install-transaction.ts` (una en `validate`, otra en
103
+ * `stage`), y agregar un consumidor mas —el chequeo de frescura del diagnostico— habria
104
+ * hecho tres. Es exactamente la duplicacion que el comentario de arriba de este modulo
105
+ * describe como el bug que costo una release.
106
+ *
107
+ * Puro: lee la fuente y devuelve texto. No escribe nada, asi que sirve igual para
108
+ * instalar, para validar antes de instalar, y para preguntar "¿lo instalado sigue
109
+ * coincidiendo con la fuente?".
110
+ */
111
+ function renderArtifact(renderer, sourcePath) {
112
+ switch (renderer) {
113
+ case 'link':
114
+ return null;
115
+ case 'codex-agent-toml':
116
+ return (0, codex_agent_1.renderCodexAgent)(fs_1.default.readFileSync(sourcePath, 'utf8'));
117
+ case 'cursor-mdc':
118
+ return (0, cursor_mdc_1.renderCursorMdc)(readSkillMd(sourcePath));
119
+ case 'copilot-instructions':
120
+ return (0, copilot_instructions_1.renderCopilotInstructions)(readSkillMd(sourcePath));
121
+ }
122
+ }
123
+ /** Las skills se rinden desde el `SKILL.md` de su directorio; los agentes de Codex,
124
+ * desde el archivo directo. La diferencia la sabe cada rama de `renderArtifact`. */
125
+ function readSkillMd(sourceDir) {
126
+ return fs_1.default.readFileSync(path_1.default.join(sourceDir, 'SKILL.md'), 'utf8');
127
+ }
@@ -244,6 +244,44 @@ describe('skillsGlobalCheck — renderer-aware (Task 4.4 / deferred Task 4.3 fin
244
244
  expect(skillsCheck?.detail).toBeUndefined();
245
245
  fs_1.default.rmSync(content, { recursive: true, force: true });
246
246
  });
247
+ it('a rendered file that no longer matches its registry source is reported stale', () => {
248
+ // El caso que C4 realmente pedia. Un symlink apunta al registry, asi que
249
+ // `awm update` lo actualiza solo — por eso claude-code/codex/opencode nunca
250
+ // quedan viejos. Un `.mdc` es un archivo GENERADO: se queda con el contenido de
251
+ // la version anterior hasta que alguien corra `awm sync`, y nada lo decia.
252
+ const source = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-stale-src-'));
253
+ fs_1.default.mkdirSync(path_1.default.join(source, 'using-awm'), { recursive: true });
254
+ const skillMd = path_1.default.join(source, 'using-awm', 'SKILL.md');
255
+ fs_1.default.writeFileSync(skillMd, '---\nname: using-awm\ndescription: original\n---\n\nCuerpo v1.\n');
256
+ const rulesDir = path_1.default.join(tmpHome, '.cursor/rules');
257
+ fs_1.default.mkdirSync(rulesDir, { recursive: true });
258
+ const target = path_1.default.join(rulesDir, 'using-awm.mdc');
259
+ const { renderArtifact } = require('../../../src/core/renderers/registry');
260
+ fs_1.default.writeFileSync(target, renderArtifact('cursor-mdc', path_1.default.join(source, 'using-awm')));
261
+ const stateDir = path_1.default.join(tmpHome, '.awm', 'state');
262
+ fs_1.default.mkdirSync(stateDir, { recursive: true });
263
+ fs_1.default.writeFileSync(path_1.default.join(stateDir, 'artifacts.json'), JSON.stringify([{
264
+ name: 'using-awm', type: 'skill', scope: 'global',
265
+ targetPath: target, sourcePath: path_1.default.join(source, 'using-awm'),
266
+ renderer: 'cursor-mdc', owners: ['cursor'],
267
+ }]));
268
+ const scanSkills = jest.fn(() => ({ valid: [], repairable: [], dead: [], usurped: [] }));
269
+ const { gatherProviderChecks } = require('../../../src/core/diagnostics/provider-checks');
270
+ // Recien instalado: coincide con su fuente.
271
+ const fresh = gatherProviderChecks(['cursor'], scanSkills)[0]
272
+ .checks.find((c) => c.id === 'skills.global');
273
+ expect(fresh?.state).toBe('supported');
274
+ // `awm update` trae una version nueva de la skill upstream.
275
+ fs_1.default.writeFileSync(skillMd, '---\nname: using-awm\ndescription: original\n---\n\nCuerpo v2, cambiado upstream.\n');
276
+ const stale = gatherProviderChecks(['cursor'], scanSkills)[0]
277
+ .checks.find((c) => c.id === 'skills.global');
278
+ expect(stale?.state).toBe('stale');
279
+ expect(stale?.detail).toContain('using-awm.mdc');
280
+ // Medido, no supuesto: de `init`/`update`/`sync`/`add`, solo `add` refresca un
281
+ // renderizado viejo. Los otros tres corren limpios y no cambian nada.
282
+ expect(stale?.remediationCode).toBe('reinstall-bundle');
283
+ fs_1.default.rmSync(source, { recursive: true, force: true });
284
+ });
247
285
  it('non-link renderer with an empty/missing dir reports absent, not a false healthy', () => {
248
286
  const scanSkills = jest.fn(() => ({ valid: [], repairable: [], dead: [], usurped: [] }));
249
287
  const { gatherProviderChecks } = require('../../../src/core/diagnostics/provider-checks');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentic-workflow-manager",
3
- "version": "6.1.2",
3
+ "version": "6.2.0",
4
4
  "main": "dist/src/index.js",
5
5
  "bin": {
6
6
  "awm": "./dist/src/index.js"