agentic-workflow-manager 6.1.0 → 6.1.2

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.
@@ -99,6 +99,19 @@ function checkBudget(cwd) {
99
99
  const config = readConfig(cwd);
100
100
  if (!config) {
101
101
  const { total, breakdown } = measure(cwd, exports.DEFAULT_FILES);
102
+ // Fijar sobre CERO archivos es una trampa, no una linea base.
103
+ //
104
+ // En un proyecto recien inicializado ninguno de los tres existe todavia: los
105
+ // escribe una sesion de agente, y `awm init` los reporta como pasos `pending`.
106
+ // El 0KB no es un error de medicion — no hay nada que medir. Pero dejar
107
+ // `maxBytes: 0` en disco hace que la corrida SIGUIENTE, apenas el agente escribe
108
+ // AGENTS.md (el flujo documentado), reporte "excedido". Una alarma que siempre
109
+ // suena se aprende a ignorar, y ahi se pierde el gate entero.
110
+ //
111
+ // Se reporta y NO se escribe config: el proximo run, ya con contexto, fija bien.
112
+ if (breakdown.length === 0) {
113
+ return { status: 'unmeasurable', totalBytes: 0, maxBytes: 0, breakdown };
114
+ }
102
115
  writeConfig(cwd, { files: exports.DEFAULT_FILES, maxBytes: total });
103
116
  return { status: 'pinned', totalBytes: total, maxBytes: total, breakdown };
104
117
  }
@@ -21,6 +21,14 @@ function exitCodeFor(report) {
21
21
  function formatReport(report) {
22
22
  const tokens = `~${(0, budget_1.estimateTokens)(report.totalBytes)}k tokens`;
23
23
  const breakdown = report.breakdown.map(b => `${b.file} ${KB(b.bytes)}`).join(', ');
24
+ if (report.status === 'unmeasurable') {
25
+ // Ni verde ni alarma: no hay nada que reportar todavia, y decir "0KB fijado"
26
+ // seria afirmar una medicion que no ocurrio.
27
+ return `${picocolors_1.default.dim('·')} Nothing to measure yet: none of ${budget_1.DEFAULT_FILES.join(', ')} exists.\n`
28
+ + ` These are written by an agent session (\`awm init\` lists them as pending).\n`
29
+ + ` Re-run this once they exist — pinning a budget of 0 would report every\n`
30
+ + ` later run as over budget.\n`;
31
+ }
24
32
  if (report.status === 'pinned') {
25
33
  return `${picocolors_1.default.green('✔')} Context budget pinned at ${KB(report.totalBytes)} (${tokens} per session).\n`
26
34
  + ` ${breakdown}\n`
@@ -73,7 +73,12 @@ function installClaudeHook(options) {
73
73
  settings.hooks = {};
74
74
  if (!settings.hooks[config.eventName])
75
75
  settings.hooks[config.eventName] = [];
76
- const entries = settings.hooks[config.eventName];
76
+ // Misma poda que en codex.ts: restos nuestros apuntando a un AWM_HOME ya borrado.
77
+ // `settings.hooks[eventName]` se reasigna porque el objeto se serializa entero abajo.
78
+ const entries = settings.hooks[config.eventName]
79
+ .filter((e) => !(0, shared_1.isDeadAwmHookEntry)(e, config.matcher, 'run-hook.cmd', config.scriptsDir));
80
+ const pruned = settings.hooks[config.eventName].length !== entries.length;
81
+ settings.hooks[config.eventName] = entries;
77
82
  const awmEntryIdx = entries.findIndex((e) => isAwmEntry(e, config.scriptsDir, config.matcher));
78
83
  const newEntry = {
79
84
  matcher: config.matcher,
@@ -85,7 +90,9 @@ function installClaudeHook(options) {
85
90
  };
86
91
  let status;
87
92
  if (awmEntryIdx >= 0) {
88
- if (JSON.stringify(entries[awmEntryIdx]) === JSON.stringify(newEntry)) {
93
+ // Igual que en codex.ts: si la poda saco algo, hay que escribir aunque nuestra
94
+ // entrada ya este identica — si no, la limpieza se calcula y se tira.
95
+ if (!pruned && JSON.stringify(entries[awmEntryIdx]) === JSON.stringify(newEntry)) {
89
96
  return { status: 'already-up-to-date', scriptsDir: config.scriptsDir, settingsPath: config.settingsPath, backupPath: null };
90
97
  }
91
98
  entries[awmEntryIdx] = newEntry;
@@ -56,13 +56,21 @@ function installCodexHook(options) {
56
56
  const hooks = current.hooks && typeof current.hooks === 'object'
57
57
  ? current.hooks
58
58
  : {};
59
- const entries = Array.isArray(hooks.SessionStart) ? hooks.SessionStart : [];
59
+ const raw = Array.isArray(hooks.SessionStart) ? hooks.SessionStart : [];
60
+ // Se podan primero las entradas nuestras que apuntan a un script inexistente: son
61
+ // restos de otro AWM_HOME ya borrado, y el agente las intenta ejecutar cada sesion.
62
+ // Una instalacion paralela VIVA no se toca — su script existe. Ver isDeadAwmHookEntry.
63
+ const entries = raw.filter((entry) => !(0, shared_1.isDeadAwmHookEntry)(entry, codexMatcher(), 'session-start', config.scriptsDir));
60
64
  const matches = entries.filter((entry) => isAwmCodexEntry(entry, config.scriptsDir));
61
65
  if (matches.length > 1) {
62
66
  throw new Error('multiple AWM SessionStart entries in Codex hooks.json');
63
67
  }
64
68
  const newEntry = awmCodexEntry(config.scriptsDir);
65
- if (matches.length === 1 && JSON.stringify(matches[0]) === JSON.stringify(newEntry)) {
69
+ // `pruned` gana sobre el early-return: si se saco basura hay que ESCRIBIRLA, aunque
70
+ // nuestra entrada ya este identica. Sin esto la poda se calculaba y se tiraba, y el
71
+ // archivo quedaba igual — lo detecto su propio test.
72
+ const pruned = raw.length !== entries.length;
73
+ if (!pruned && matches.length === 1 && JSON.stringify(matches[0]) === JSON.stringify(newEntry)) {
66
74
  return { status: 'already-up-to-date', scriptsDir: config.scriptsDir, settingsPath: config.settingsPath, backupPath: null };
67
75
  }
68
76
  const nextEntries = matches.length === 1
@@ -14,6 +14,7 @@ exports.backupManagedFile = backupManagedFile;
14
14
  exports.readStrictJson = readStrictJson;
15
15
  exports.checkExecutable = checkExecutable;
16
16
  exports.checkFile = checkFile;
17
+ exports.isDeadAwmHookEntry = isDeadAwmHookEntry;
17
18
  const fs_1 = __importDefault(require("fs"));
18
19
  const path_1 = __importDefault(require("path"));
19
20
  const paths_1 = require("../../core/paths");
@@ -110,3 +111,44 @@ function checkFile(file) {
110
111
  return { ok: false, detail: `broken link: ${file}` };
111
112
  }
112
113
  }
114
+ /**
115
+ * Entradas de hook con NUESTRA forma cuyo script ya no existe en disco.
116
+ *
117
+ * `awm init` reconoce como propia solo la entrada que apunta al `AWM_HOME` actual, asi
118
+ * que instalar con otro `AWM_HOME` AGREGA una segunda en vez de reemplazar. Una corrida
119
+ * de playbook aislada deja en el archivo real una entrada permanente hacia un directorio
120
+ * temporal ya borrado, y el agente intenta ejecutarla en cada sesion, para siempre.
121
+ * Observado en una corrida real: `~/.codex/hooks.json` con dos entradas de AWM.
122
+ *
123
+ * Tres condiciones, a proposito — es el archivo de configuracion DEL USUARIO y no se le
124
+ * borran lineas por parecido vago:
125
+ * 1. el `matcher` es exactamente el nuestro,
126
+ * 2. el ejecutable se llama como el script que AWM instala, y
127
+ * 3. esa ruta NO existe.
128
+ *
129
+ * Una instalacion paralela viva no cumple (3), asi que sobrevive intacta. Lo unico que
130
+ * se poda es basura que AWM misma dejo y que ya no puede funcionar.
131
+ */
132
+ function isDeadAwmHookEntry(entry, matcher, scriptBasename, currentScriptsDir) {
133
+ const e = entry;
134
+ if (e?.matcher !== matcher || !Array.isArray(e.hooks))
135
+ return false;
136
+ return e.hooks.some((h) => {
137
+ const command = h?.command;
138
+ if (typeof command !== 'string' || command.length === 0)
139
+ return false;
140
+ // Claude invoca `<ruta>/run-hook.cmd session-start`: el ejecutable es el primer
141
+ // token. Codex invoca la ruta pelada. Partir por espacio cubre las dos formas.
142
+ const executable = command.split(' ')[0];
143
+ if (path_1.default.basename(executable) !== scriptBasename)
144
+ return false;
145
+ // La entrada del AWM_HOME ACTUAL nunca es basura, aunque el script todavia no
146
+ // este en disco: en una instalacion nueva el archivo aparece unos pasos despues.
147
+ // Sin esta guarda, un `hooks.json` con dos entradas duplicadas del home actual se
148
+ // podaba entero y el guard de duplicados (R17) dejaba de dispararse — lo detecto
149
+ // su propio test al agregar la poda.
150
+ if (path_1.default.dirname(executable) === currentScriptsDir)
151
+ return false;
152
+ return !fs_1.default.existsSync(executable);
153
+ });
154
+ }
@@ -60,13 +60,13 @@ function binaryVersionCheck(agent) {
60
60
  * `dir` is null — i.e. the provider has no global skill discovery mechanism at all
61
61
  * (today: Copilot, see `globalUnsupportedReason` in providers/index.ts).
62
62
  *
63
- * `renderer` gates which verification is possible: `classifySkillLinks` (via `integrity`)
64
- * only ever sees symlinks — `if (!lst.isSymbolicLink()) continue;` — so for a rendered
65
- * format (`cursor-mdc`, `copilot-instructions`) it scans a directory of real files and
66
- * finds nothing, which would make `broken` silently read 0 regardless of whether the
67
- * rendered files are actually well-formed. Reporting `'healthy'` from a scan that
68
- * structurally can't see the files it's supposed to check would be a false green, so for
69
- * any non-`'link'` renderer this reports presence only, honestly, via `detail`. */
63
+ * `renderer` parte la verificacion en dos caminos distintos porque los artefactos son
64
+ * distintos: `classifySkillLinks` solo ve symlinks — `if (!lst.isSymbolicLink()) continue;`
65
+ * asi que sobre un directorio de archivos renderizados no encuentra nada. Durante un
66
+ * tiempo esa rama reporto SOLO presencia, y lo decia en el `detail` para no fingir una
67
+ * verificacion que no ocurria. Ahora tambien comprueba contenido, con el marcador que
68
+ * declara el renderer (`rendererIntegrityMarker`): un archivo con la extension correcta
69
+ * y el cuerpo vacio o truncado ya no pasa como sano. */
70
70
  function skillsGlobalCheck(dir, owners, integrity, renderer) {
71
71
  if (dir === null)
72
72
  return null;
@@ -88,23 +88,37 @@ function skillsGlobalCheck(dir, owners, integrity, renderer) {
88
88
  // Require at least one entry with the extension this renderer actually produces,
89
89
  // not just ANY file — a directory non-empty only because of the user's own
90
90
  // pre-existing, unrelated rule/instructions file must not read as "AWM installed".
91
- // Still not full integrity verification (a stray file with the right extension but
92
- // wrong content still passes) — that residual gap is the same honest tradeoff this
93
- // function's doc comment already accepts for the non-`'link'` branch generally.
94
91
  // `rendererExtension` (core/renderers/registry.ts) is the ONE table mapping a
95
92
  // renderer to the file it produces. This site used to keep its own partial copy —
96
93
  // the fourth such copy in the codebase, and the exact drift that made a rendered
97
94
  // artifact invisible to the "is it installed" check for a whole release. A new
98
95
  // renderer must not be able to be added without this reading it.
99
96
  const ext = (0, registry_1.rendererExtension)(renderer);
100
- const present = ext ? entries.some((e) => e.endsWith(ext)) : entries.length > 0;
97
+ const rendered = ext ? entries.filter((e) => e.endsWith(ext)) : entries;
98
+ const present = rendered.length > 0;
99
+ if (!present) {
100
+ return {
101
+ id: 'skills.global', state: 'absent', target: dir,
102
+ owners: owners.length > 1 ? owners : undefined,
103
+ remediationCode: 'awm-init',
104
+ };
105
+ }
106
+ // Ya no se reporta "content integrity not verified": AHORA se verifica.
107
+ // Comprobar presencia y extension dejaba pasar un archivo correcto por fuera y
108
+ // vacio o truncado por dentro — el agente cargaba nada y doctor decia que si.
109
+ const corrupt = corruptRendered(dir, rendered, renderer);
101
110
  return {
102
111
  id: 'skills.global',
103
- state: present ? 'supported' : 'absent',
112
+ state: corrupt.length > 0 ? 'broken' : 'supported',
104
113
  target: dir,
105
114
  owners: owners.length > 1 ? owners : undefined,
106
- detail: present ? 'rendered install — content integrity not verified' : undefined,
107
- remediationCode: present ? undefined : 'awm-init',
115
+ detail: corrupt.length > 0
116
+ ? `${corrupt.length} rendered file(s) missing their expected content (${corrupt.slice(0, 3).join(', ')})`
117
+ : 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,
108
122
  };
109
123
  }
110
124
  const shared = owners.length > 1;
@@ -153,21 +167,29 @@ function skillsGlobalCheck(dir, owners, integrity, renderer) {
153
167
  : broken > 0 ? 'repair-global-skills' : undefined,
154
168
  };
155
169
  }
156
- /** R8: verify the Codex `.toml` agents this run's renderer would have produced still parse. */
157
- function tomlAgentsHealthy(dir, entries) {
158
- const tomls = entries.filter((e) => e.endsWith('.toml'));
159
- const broken = tomls.filter((file) => {
170
+ /**
171
+ * Archivos renderizados cuyo contenido ya no es lo que el renderer escribe.
172
+ *
173
+ * El marcador sale de `rendererIntegrityMarker` — la MISMA tabla que da la extension —
174
+ * y no de una copia local. Antes solo `codex-agent-toml` tenia esta verificacion, con su
175
+ * marcador horneado en esta funcion: el tercer renderer en agregarse habria pasado sin
176
+ * verificar y nadie se habria enterado.
177
+ *
178
+ * Un archivo ilegible cuenta como corrupto: no poder leerlo no es evidencia de que este
179
+ * bien. Es la misma disciplina que el resto de los checks — nunca verde por no mirar.
180
+ */
181
+ function corruptRendered(dir, files, renderer) {
182
+ const marker = (0, registry_1.rendererIntegrityMarker)(renderer);
183
+ if (marker === null)
184
+ return [];
185
+ return files.filter((file) => {
160
186
  try {
161
- const content = fs_1.default.readFileSync(path_1.default.join(dir, file), 'utf8');
162
- // renderCodexAgent (Task 4) always emits this key — its absence means the
163
- // file was hand-edited or truncated, not a shape `awm` would have written.
164
- return !content.includes('developer_instructions = ');
187
+ return !fs_1.default.readFileSync(path_1.default.join(dir, file), 'utf8').includes(marker);
165
188
  }
166
189
  catch {
167
190
  return true;
168
191
  }
169
- }).length;
170
- return { broken };
192
+ });
171
193
  }
172
194
  /**
173
195
  * Returns `null` when a check doesn't structurally apply to `agent` (e.g. Antigravity
@@ -255,7 +277,10 @@ function agentsNativeCheck(agent) {
255
277
  if (entries.length === 0)
256
278
  return null;
257
279
  if (provider.agent.renderer === 'codex-agent-toml') {
258
- const { broken } = tomlAgentsHealthy(dir, entries);
280
+ // La extension sale de la tabla, no de un literal: escribir '.toml' aca fue
281
+ // exactamente lo que el guard estructural existe para detener, y lo detuvo.
282
+ const tomlExt = (0, registry_1.rendererExtension)('codex-agent-toml');
283
+ const broken = corruptRendered(dir, entries.filter((e) => e.endsWith(tomlExt)), 'codex-agent-toml').length;
259
284
  return {
260
285
  id: 'agents.native',
261
286
  state: broken > 0 ? 'broken' : 'healthy',
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.renderedFilename = renderedFilename;
4
4
  exports.rendererExtension = rendererExtension;
5
+ exports.rendererIntegrityMarker = rendererIntegrityMarker;
5
6
  /** Extension que el renderer estampa sobre el nombre base, o `null` cuando el
6
7
  * artefacto se instala con el nombre tal cual (renderer `link`). */
7
8
  const RENDERER_EXTENSION = {
@@ -10,6 +11,26 @@ const RENDERER_EXTENSION = {
10
11
  'cursor-mdc': '.mdc',
11
12
  'copilot-instructions': '.instructions.md',
12
13
  };
14
+ /**
15
+ * Cadena que el renderer SIEMPRE estampa en lo que produce, y que por lo tanto
16
+ * distingue "el archivo existe" de "el archivo sigue siendo lo que escribimos".
17
+ *
18
+ * Vive en esta tabla, al lado de la extension, por la misma razon que la extension:
19
+ * son los dos hechos que un renderer nuevo tiene que declarar, y tenerlos juntos hace
20
+ * imposible agregar uno y olvidarse del segundo. `codex-agent-toml` ya tenia su marcador
21
+ * — horneado dentro de `tomlAgentsHealthy` en provider-checks.ts — y era el UNICO de los
22
+ * tres renderers con verificacion de contenido. Los otros dos comprobaban presencia y
23
+ * extension: un archivo correcto por fuera y vacio o truncado por dentro pasaba como sano.
24
+ *
25
+ * `null` en `link`: ahi la integridad la responde el clasificador de symlinks, que
26
+ * ademas distingue colgante de usurpado (D-007). Un marcador de texto no aplica.
27
+ */
28
+ const RENDERER_INTEGRITY_MARKER = {
29
+ link: null,
30
+ 'codex-agent-toml': 'developer_instructions = ',
31
+ 'cursor-mdc': 'alwaysApply:',
32
+ 'copilot-instructions': 'applyTo:',
33
+ };
13
34
  /** `.md` es la unica extension que un `installName` de artefacto puede traer.
14
35
  * Deliberadamente NO se usa `path.parse().name`: eso corta desde el ULTIMO
15
36
  * punto, asi que un skill llamado `v1.2-migration` se truncaria a `v1`,
@@ -35,3 +56,8 @@ function renderedFilename(installName, renderer) {
35
56
  function rendererExtension(renderer) {
36
57
  return RENDERER_EXTENSION[renderer];
37
58
  }
59
+ /** Marcador de integridad del renderer, o `null` cuando no aplica (`link`).
60
+ * Ver `RENDERER_INTEGRITY_MARKER`. */
61
+ function rendererIntegrityMarker(renderer) {
62
+ return RENDERER_INTEGRITY_MARKER[renderer];
63
+ }
@@ -8,6 +8,7 @@ const os_1 = __importDefault(require("os"));
8
8
  const path_1 = __importDefault(require("path"));
9
9
  const budget_1 = require("../../../src/commands/context-budget/budget");
10
10
  const context_budget_1 = require("../../../src/commands/context-budget");
11
+ const tmp_1 = require("../../support/tmp");
11
12
  function project(files) {
12
13
  // CLAUDE.md: no test may reach the real ~/.awm. Everything here is a tmpdir.
13
14
  const dir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-budget-'));
@@ -98,3 +99,38 @@ describe('reporting', () => {
98
99
  expect(out).toMatch(/~5[0-9]k tokens/);
99
100
  });
100
101
  });
102
+ // Fijar sobre cero archivos garantiza una falsa alarma en la corrida siguiente.
103
+ //
104
+ // `context-budget` mide AGENTS.md / CONSTITUTION.md / CLAUDE.md. En un proyecto recien
105
+ // inicializado NINGUNO existe: los escribe una sesion de agente, y `awm init` los reporta
106
+ // como pasos `pending`. El 0KB no es un error de medicion — no hay nada que medir. El
107
+ // problema es FIJAR sobre eso: apenas el agente escribe AGENTS.md, que es el flujo
108
+ // documentado, el comando reporta "excedido". Una alarma que siempre suena se aprende a
109
+ // ignorar. Ver issue #56.
110
+ describe('pinning refuses to happen when there is nothing to measure', () => {
111
+ let cwd;
112
+ beforeEach(() => { cwd = (0, tmp_1.mkCanonicalTmpDir)('awm-budget-zero-'); });
113
+ afterEach(() => { fs_1.default.rmSync(cwd, { recursive: true, force: true }); });
114
+ it('does not pin a budget on a project whose context files do not exist yet', () => {
115
+ const report = (0, budget_1.checkBudget)(cwd);
116
+ expect(report.status).toBe('unmeasurable');
117
+ expect(report.totalBytes).toBe(0);
118
+ // Lo que importa: NO deja config. Un `maxBytes: 0` en disco es la trampa.
119
+ expect(fs_1.default.existsSync(path_1.default.join(cwd, budget_1.CONFIG_FILE))).toBe(false);
120
+ });
121
+ it('the very next run, after the agent writes AGENTS.md, is not "over budget"', () => {
122
+ (0, budget_1.checkBudget)(cwd); // primer intento: nada que medir
123
+ fs_1.default.writeFileSync(path_1.default.join(cwd, 'AGENTS.md'), 'x'.repeat(3000));
124
+ const report = (0, budget_1.checkBudget)(cwd);
125
+ // Este es el bug entero: antes daba 'over' — 3KB contra un maximo de 0.
126
+ expect(report.status).toBe('pinned');
127
+ expect(report.maxBytes).toBe(3000);
128
+ });
129
+ it('still pins normally when at least one context file exists', () => {
130
+ fs_1.default.writeFileSync(path_1.default.join(cwd, 'CLAUDE.md'), 'y'.repeat(500));
131
+ const report = (0, budget_1.checkBudget)(cwd);
132
+ expect(report.status).toBe('pinned');
133
+ expect(report.maxBytes).toBe(500);
134
+ expect(fs_1.default.existsSync(path_1.default.join(cwd, budget_1.CONFIG_FILE))).toBe(true);
135
+ });
136
+ });
@@ -248,6 +248,60 @@ describe('installHook / computeHookStatus / uninstallHook — Codex adapter', ()
248
248
  expect(result.status).toBe('not-installed');
249
249
  expect(result.backupPath).toBeNull();
250
250
  });
251
+ // Restos de otro AWM_HOME: se podan, pero solo si estan MUERTOS.
252
+ //
253
+ // `awm init` reconoce como propia solo la entrada del AWM_HOME actual, asi que instalar
254
+ // con otro AGREGA una segunda. Una corrida de playbook aislada deja en el archivo real
255
+ // una entrada permanente hacia un directorio temporal ya borrado, y el agente la intenta
256
+ // ejecutar cada sesion. Observado: `~/.codex/hooks.json` con dos entradas de AWM. Ver
257
+ // backlog §C2.
258
+ describe('stale AWM entries from another AWM_HOME are pruned, live ones are not', () => {
259
+ /** Una entrada con nuestra forma apuntando a `scriptPath`. */
260
+ function entryFor(scriptPath) {
261
+ return {
262
+ matcher: 'startup|resume|clear|compact',
263
+ hooks: [{ type: 'command', command: scriptPath, statusMessage: 'Loading AWM session state' }],
264
+ };
265
+ }
266
+ it('drops an entry whose script no longer exists', () => {
267
+ const { isDeadAwmHookEntry } = require('../../../src/commands/hooks/shared');
268
+ const dead = entryFor('/tmp/awm-home-that-was-deleted/hooks/codex/session-start');
269
+ expect(isDeadAwmHookEntry(dead, 'startup|resume|clear|compact', 'session-start', codexScriptsDir)).toBe(true);
270
+ });
271
+ it('keeps a parallel install whose script DOES exist', () => {
272
+ // Es la diferencia entre limpiar basura propia y romperle la instalacion a alguien.
273
+ const { isDeadAwmHookEntry } = require('../../../src/commands/hooks/shared');
274
+ const alive = entryFor(path_1.default.join(codexScriptsDir, 'session-start'));
275
+ fs_1.default.mkdirSync(codexScriptsDir, { recursive: true });
276
+ fs_1.default.writeFileSync(path_1.default.join(codexScriptsDir, 'session-start'), '#!/bin/sh\n', { mode: 0o755 });
277
+ expect(isDeadAwmHookEntry(alive, 'startup|resume|clear|compact', 'session-start', codexScriptsDir)).toBe(false);
278
+ });
279
+ it('leaves a foreign hook alone even when its path is dead', () => {
280
+ // Mismo matcher, ruta muerta, pero NO es nuestro script: no se toca.
281
+ const { isDeadAwmHookEntry } = require('../../../src/commands/hooks/shared');
282
+ const foreign = entryFor('/tmp/someone-elses/my-own-hook');
283
+ expect(isDeadAwmHookEntry(foreign, 'startup|resume|clear|compact', 'session-start', codexScriptsDir)).toBe(false);
284
+ });
285
+ it('install removes the stale entry instead of accumulating a second one', () => {
286
+ installCodexFixture({ heartbeat: false });
287
+ const hooksJson = path_1.default.join(tmpHome, '.codex', 'hooks.json');
288
+ const current = JSON.parse(fs_1.default.readFileSync(hooksJson, 'utf-8'));
289
+ // Simular la corrida anterior con otro AWM_HOME, ya borrado.
290
+ current.hooks.SessionStart.unshift(entryFor('/tmp/awm-e2e-gone/hooks/codex/session-start'));
291
+ fs_1.default.writeFileSync(hooksJson, JSON.stringify(current, null, 2));
292
+ expect(JSON.parse(fs_1.default.readFileSync(hooksJson, 'utf-8')).hooks.SessionStart).toHaveLength(2);
293
+ writeRegistry();
294
+ const { installHook } = require('../../../src/commands/hooks/install');
295
+ installHook({ agent: 'codex', registryRoot: tmpRegistry, installMethod: 'copy' });
296
+ const after = JSON.parse(fs_1.default.readFileSync(hooksJson, 'utf-8')).hooks.SessionStart;
297
+ expect(after).toHaveLength(1);
298
+ // Sobre la ESTRUCTURA, no sobre el JSON serializado: en Windows las barras
299
+ // invertidas van escapadas dentro del string, asi que un `toContain(tmpHome)`
300
+ // compara `C:\Users\...` contra `C:\\Users\\...` y falla sobre un producto
301
+ // que se comporta bien. Paso en CI — `platform-property-assumed-universal`.
302
+ expect(after[0].hooks[0].command).toBe(path_1.default.join(codexScriptsDir, 'session-start'));
303
+ });
304
+ });
251
305
  });
252
306
  // El remedio tiene que ser ejecutable, no un codigo.
253
307
  //
@@ -183,7 +183,24 @@ describe('skillsGlobalCheck — renderer-aware (Task 4.4 / deferred Task 4.3 fin
183
183
  const skillsCheck = facts[0].checks.find((c) => c.id === 'skills.global');
184
184
  expect(skillsCheck?.state).not.toBe('healthy');
185
185
  expect(skillsCheck?.state).toBe('supported');
186
- expect(skillsCheck?.detail).toContain('not verified');
186
+ // Antes decia 'content integrity not verified' — honesto entonces, porque no se
187
+ // verificaba. Ahora SI se verifica, y este archivo tiene su marcador, asi que no
188
+ // hay nada que reportar.
189
+ expect(skillsCheck?.detail).toBeUndefined();
190
+ });
191
+ it('a rendered file with the right extension but a truncated body is reported broken', () => {
192
+ // El hueco que cerraba C3: presencia + extension dejaba pasar un archivo correcto
193
+ // por fuera y vacio por dentro. El agente cargaba nada y doctor decia que si.
194
+ const rulesDir = path_1.default.join(tmpHome, '.cursor/rules');
195
+ fs_1.default.mkdirSync(rulesDir, { recursive: true });
196
+ fs_1.default.writeFileSync(path_1.default.join(rulesDir, 'development-process.mdc'), '---\ndescription: x\n');
197
+ const scanSkills = jest.fn(() => ({ valid: [], repairable: [], dead: [], usurped: [] }));
198
+ const { gatherProviderChecks } = require('../../../src/core/diagnostics/provider-checks');
199
+ const facts = gatherProviderChecks(['cursor'], scanSkills);
200
+ const skillsCheck = facts[0].checks.find((c) => c.id === 'skills.global');
201
+ expect(skillsCheck?.state).toBe('broken');
202
+ expect(skillsCheck?.detail).toContain('development-process.mdc');
203
+ expect(skillsCheck?.remediationCode).toBe('reinstall-rendered-artifacts');
187
204
  });
188
205
  it('Gap B — non-link renderer (cursor-mdc) against REAL renderer/pipeline output, not a hand-written approximation', () => {
189
206
  // The test above hand-writes a `.mdc` file whose frontmatter shape is only an
@@ -221,7 +238,10 @@ describe('skillsGlobalCheck — renderer-aware (Task 4.4 / deferred Task 4.3 fin
221
238
  const facts = gatherProviderChecks(['cursor'], scanSkills);
222
239
  const skillsCheck = facts[0].checks.find((c) => c.id === 'skills.global');
223
240
  expect(skillsCheck).toMatchObject({ id: 'skills.global', state: 'supported', target: rulesDir });
224
- expect(skillsCheck?.detail).toContain('not verified');
241
+ // La verificacion de contenido corre sobre la salida REAL del renderer, no sobre
242
+ // una aproximacion escrita a mano: si el marcador declarado en la tabla no
243
+ // coincidiera con lo que el renderer emite de verdad, este test lo detectaria.
244
+ expect(skillsCheck?.detail).toBeUndefined();
225
245
  fs_1.default.rmSync(content, { recursive: true, force: true });
226
246
  });
227
247
  it('non-link renderer with an empty/missing dir reports absent, not a false healthy', () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentic-workflow-manager",
3
- "version": "6.1.0",
3
+ "version": "6.1.2",
4
4
  "main": "dist/src/index.js",
5
5
  "bin": {
6
6
  "awm": "./dist/src/index.js"