agentic-workflow-manager 9.11.6 → 9.11.7

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.
@@ -10,6 +10,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.awmCodexEntry = awmCodexEntry;
13
+ exports.isStaleAwmCodexEntry = isStaleAwmCodexEntry;
13
14
  exports.installCodexHook = installCodexHook;
14
15
  exports.computeCodexHookStatus = computeCodexHookStatus;
15
16
  exports.uninstallCodexHook = uninstallCodexHook;
@@ -38,6 +39,44 @@ function awmCodexEntry(scriptsDir) {
38
39
  }],
39
40
  };
40
41
  }
42
+ /**
43
+ * Una entrada NUESTRA en `SessionStart` de Codex que ya no es la canonica (#184).
44
+ *
45
+ * Medido en una Mac real: `hooks.json` con dos entradas de AWM cuyos matchers se
46
+ * solapan en startup/clear/compact, o sea el hook corre dos veces por sesion y el
47
+ * contexto se inyecta dos veces. Ninguna de las dos vias existentes la alcanzaba:
48
+ * `isAwmCodexEntry` exige el matcher ACTUAL, e `isDeadAwmHookEntry` compara el
49
+ * basename del ejecutable contra 'session-start' cuando aca el ejecutable es
50
+ * `run-hook.cmd`. Y no es "muerta": el wrapper existe porque Claude Code si lo usa,
51
+ * asi que ninguna prueba de vivencia puede alcanzarla.
52
+ *
53
+ * Se reconoce por PROCEDENCIA y no-canonicidad, en dos formas:
54
+ *
55
+ * - `run-hook.cmd session-start`: el contrato de wrapper que escribe el adapter de
56
+ * Claude (claude.ts) y que este adapter declara no usar. En el archivo de Codex
57
+ * solo puede ser un resto de una version anterior.
58
+ * - nuestro propio script con el matcher viejo: misma ruta exacta que instalamos,
59
+ * pero el matcher cambio, asi que el instalador la ignoraba y AGREGABA otra.
60
+ *
61
+ * Deliberadamente fuera: una instalacion paralela VIVA de otro AWM_HOME, que usa la
62
+ * forma nativa con su propia ruta. `isDeadAwmHookEntry` la preserva a proposito y
63
+ * esta funcion no revierte esa decision.
64
+ */
65
+ function isStaleAwmCodexEntry(entry, scriptsDir, canonicalMatcher) {
66
+ const candidate = entry;
67
+ if (!Array.isArray(candidate?.hooks))
68
+ return false;
69
+ const ours = path_1.default.join(scriptsDir, 'session-start');
70
+ return candidate.hooks.some((hook) => {
71
+ const command = hook?.command;
72
+ if (typeof command !== 'string' || command.length === 0)
73
+ return false;
74
+ const [executable, ...args] = command.split(' ');
75
+ if (path_1.default.basename(executable) === 'run-hook.cmd' && args.join(' ') === 'session-start')
76
+ return true;
77
+ return executable === ours && candidate.matcher !== canonicalMatcher;
78
+ });
79
+ }
41
80
  function isAwmCodexEntry(entry, scriptsDir) {
42
81
  return (entry?.matcher === codexMatcher() &&
43
82
  Array.isArray(entry?.hooks) &&
@@ -60,7 +99,8 @@ function installCodexHook(options) {
60
99
  // Se podan primero las entradas nuestras que apuntan a un script inexistente: son
61
100
  // restos de otro AWM_HOME ya borrado, y el agente las intenta ejecutar cada sesion.
62
101
  // 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));
102
+ const entries = raw.filter((entry) => !(0, shared_1.isDeadAwmHookEntry)(entry, codexMatcher(), 'session-start', config.scriptsDir)
103
+ && !isStaleAwmCodexEntry(entry, config.scriptsDir, codexMatcher()));
64
104
  const matches = entries.filter((entry) => isAwmCodexEntry(entry, config.scriptsDir));
65
105
  if (matches.length > 1) {
66
106
  throw new Error('multiple AWM SessionStart entries in Codex hooks.json');
@@ -282,6 +282,90 @@ describe('installHook / computeHookStatus / uninstallHook — Codex adapter', ()
282
282
  const foreign = entryFor('/tmp/someone-elses/my-own-hook');
283
283
  expect(isDeadAwmHookEntry(foreign, 'startup|resume|clear|compact', 'session-start', codexScriptsDir)).toBe(false);
284
284
  });
285
+ // #184, medido en una Mac real: `~/.codex/hooks.json` con DOS entradas de AWM,
286
+ // matchers solapados en startup/clear/compact, o sea el hook corre dos veces por
287
+ // sesion y el contexto se inyecta dos veces — tokens duplicados en cada arranque.
288
+ //
289
+ // La entrada vieja es la forma WRAPPER de Claude (`run-hook.cmd session-start` con
290
+ // el matcher de Claude), que este adapter declara en su propio encabezado que no
291
+ // usa. Es invisible para las dos vias que la reconocerian: `isAwmCodexEntry` exige
292
+ // el matcher actual, e `isDeadAwmHookEntry` compara el basename del EJECUTABLE
293
+ // contra 'session-start' — y aca el ejecutable es `run-hook.cmd`. Y no es "muerta":
294
+ // `run-hook.cmd` existe, porque Claude Code si lo usa, asi que ninguna prueba de
295
+ // vivencia puede alcanzarla.
296
+ function legacyClaudeWrapperEntry() {
297
+ return {
298
+ matcher: 'startup|clear|compact',
299
+ hooks: [{ type: 'command', command: `${path_1.default.join(tmpHome, '.awm/hooks/run-hook.cmd')} session-start` }],
300
+ };
301
+ }
302
+ it('drops the legacy run-hook.cmd wrapper entry, so the hook stops firing twice', () => {
303
+ installCodexFixture({ heartbeat: false });
304
+ // El wrapper EXISTE en disco: es el que usa Claude Code de verdad.
305
+ fs_1.default.mkdirSync(path_1.default.join(tmpHome, '.awm/hooks'), { recursive: true });
306
+ fs_1.default.writeFileSync(path_1.default.join(tmpHome, '.awm/hooks/run-hook.cmd'), '#!/bin/sh\n', { mode: 0o755 });
307
+ const current = JSON.parse(fs_1.default.readFileSync(hooksJson, 'utf-8'));
308
+ current.hooks.SessionStart.unshift(legacyClaudeWrapperEntry());
309
+ fs_1.default.writeFileSync(hooksJson, JSON.stringify(current, null, 2));
310
+ writeRegistry();
311
+ const { installHook } = require('../../../src/commands/hooks/install');
312
+ installHook({ agent: 'codex', registryRoot: tmpRegistry, installMethod: 'copy' });
313
+ const after = JSON.parse(fs_1.default.readFileSync(hooksJson, 'utf-8')).hooks.SessionStart;
314
+ expect(after).toHaveLength(1);
315
+ expect(after[0].matcher).toBe('startup|resume|clear|compact');
316
+ expect(after[0].hooks[0].command).toBe(path_1.default.join(codexScriptsDir, 'session-start'));
317
+ });
318
+ it('drops our own entry when only its matcher drifted, instead of adding a second one', () => {
319
+ installCodexFixture({ heartbeat: false });
320
+ const current = JSON.parse(fs_1.default.readFileSync(hooksJson, 'utf-8'));
321
+ current.hooks.SessionStart.unshift({
322
+ matcher: 'startup|clear|compact',
323
+ hooks: [{ type: 'command', command: path_1.default.join(codexScriptsDir, 'session-start') }],
324
+ });
325
+ fs_1.default.writeFileSync(hooksJson, JSON.stringify(current, null, 2));
326
+ writeRegistry();
327
+ const { installHook } = require('../../../src/commands/hooks/install');
328
+ installHook({ agent: 'codex', registryRoot: tmpRegistry, installMethod: 'copy' });
329
+ const after = JSON.parse(fs_1.default.readFileSync(hooksJson, 'utf-8')).hooks.SessionStart;
330
+ expect(after).toHaveLength(1);
331
+ expect(after[0].matcher).toBe('startup|resume|clear|compact');
332
+ });
333
+ it('leaves a live parallel AWM_HOME alone: cleaning our own leftovers is not breaking someone else', () => {
334
+ installCodexFixture({ heartbeat: false });
335
+ const other = path_1.default.join(tmpHome, 'other-awm/hooks/codex');
336
+ fs_1.default.mkdirSync(other, { recursive: true });
337
+ fs_1.default.writeFileSync(path_1.default.join(other, 'session-start'), '#!/bin/sh\n', { mode: 0o755 });
338
+ const current = JSON.parse(fs_1.default.readFileSync(hooksJson, 'utf-8'));
339
+ current.hooks.SessionStart.unshift(entryFor(path_1.default.join(other, 'session-start')));
340
+ fs_1.default.writeFileSync(hooksJson, JSON.stringify(current, null, 2));
341
+ writeRegistry();
342
+ const { installHook } = require('../../../src/commands/hooks/install');
343
+ installHook({ agent: 'codex', registryRoot: tmpRegistry, installMethod: 'copy' });
344
+ const after = JSON.parse(fs_1.default.readFileSync(hooksJson, 'utf-8')).hooks.SessionStart;
345
+ expect(after).toHaveLength(2);
346
+ });
347
+ // La guarda real: podar por procedencia tiene que distinguir NUESTRA ruta exacta
348
+ // de la de otro AWM_HOME. Con matcher canonico la distincion no se ejercita (nada
349
+ // la poda de todos modos), asi que este caso usa un matcher DERIVADO: si la regla
350
+ // mirara el basename en vez de la ruta completa, le romperia la instalacion a otro.
351
+ it('leaves a live parallel AWM_HOME alone even when ITS matcher drifted', () => {
352
+ installCodexFixture({ heartbeat: false });
353
+ const other = path_1.default.join(tmpHome, 'other-awm/hooks/codex');
354
+ fs_1.default.mkdirSync(other, { recursive: true });
355
+ fs_1.default.writeFileSync(path_1.default.join(other, 'session-start'), '#!/bin/sh\n', { mode: 0o755 });
356
+ const current = JSON.parse(fs_1.default.readFileSync(hooksJson, 'utf-8'));
357
+ current.hooks.SessionStart.unshift({
358
+ matcher: 'startup|clear|compact',
359
+ hooks: [{ type: 'command', command: path_1.default.join(other, 'session-start') }],
360
+ });
361
+ fs_1.default.writeFileSync(hooksJson, JSON.stringify(current, null, 2));
362
+ writeRegistry();
363
+ const { installHook } = require('../../../src/commands/hooks/install');
364
+ installHook({ agent: 'codex', registryRoot: tmpRegistry, installMethod: 'copy' });
365
+ const after = JSON.parse(fs_1.default.readFileSync(hooksJson, 'utf-8')).hooks.SessionStart;
366
+ expect(after).toHaveLength(2);
367
+ expect(after.some((e) => e.hooks[0].command === path_1.default.join(other, 'session-start'))).toBe(true);
368
+ });
285
369
  it('install removes the stale entry instead of accumulating a second one', () => {
286
370
  installCodexFixture({ heartbeat: false });
287
371
  const hooksJson = path_1.default.join(tmpHome, '.codex', 'hooks.json');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentic-workflow-manager",
3
- "version": "9.11.6",
3
+ "version": "9.11.7",
4
4
  "main": "dist/src/index.js",
5
5
  "bin": {
6
6
  "awm": "./dist/src/index.js"
Binary file
Binary file