agentic-workflow-manager 6.0.0 → 6.1.1

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
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.registerHooksCommand = registerHooksCommand;
7
+ const trust_guidance_1 = require("./trust-guidance");
7
8
  const picocolors_1 = __importDefault(require("picocolors"));
8
9
  const prompts_1 = require("@clack/prompts");
9
10
  const config_1 = require("../../utils/config");
@@ -136,8 +137,8 @@ function registerHooksCommand(program) {
136
137
  : picocolors_1.default.red(result.overall);
137
138
  console.log(` Status: ${overall}`);
138
139
  if (result.overall === 'PENDING_TRUST') {
139
- console.log(picocolors_1.default.dim(' El hook esta instalado y bien formado, pero nunca se lo vio correr.'));
140
- console.log(picocolors_1.default.dim(' Abri una sesion del agente; si sigue igual, no se esta disparando.'));
140
+ for (const line of (0, trust_guidance_1.pendingTrustGuidance)(agent))
141
+ console.log(picocolors_1.default.dim(` ${line}`));
141
142
  }
142
143
  // PENDING_TRUST no sale 1: no hay nada roto que arreglar, y salir 1 en
143
144
  // toda instalacion recien hecha convertiria el comando en ruido.
@@ -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
+ }
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.pendingTrustGuidance = pendingTrustGuidance;
4
+ /**
5
+ * Que hacer con un hook instalado que nunca corrio.
6
+ *
7
+ * `doctor` emitia el codigo `open-hooks-trust` y NADA mas — ni en la referencia del CLI,
8
+ * ni en la salida, ni en la doc. El usuario leia `→ open-hooks-trust` y no tenia ninguna
9
+ * accion que tomar. Un remedio que no se puede ejecutar no es un remedio.
10
+ *
11
+ * El texto de abajo es el que Codex 0.146.0 muestra de verdad, copiado de una corrida
12
+ * observada — no una parafrasis de lo que suponemos que dice.
13
+ */
14
+ function pendingTrustGuidance(agent) {
15
+ if (agent === 'codex') {
16
+ return [
17
+ 'El hook esta instalado y registrado, pero Codex todavia no lo ejecuto.',
18
+ 'Abri una sesion de Codex en cualquier proyecto: va a mostrar',
19
+ '',
20
+ ' Hooks need review',
21
+ ' 1 hook is new or changed.',
22
+ ' Hooks can run outside the sandbox after you trust them.',
23
+ '',
24
+ 'Elegi "Trust all and continue". Desde esa sesion el hook corre y este',
25
+ 'comando pasa a HEALTHY. Si elegis "Continue without trusting", no corre.',
26
+ ];
27
+ }
28
+ return [
29
+ 'El hook esta instalado y bien formado, pero nunca se lo vio correr.',
30
+ 'Abri una sesion del agente; si sigue igual, no se esta disparando.',
31
+ ];
32
+ }
@@ -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,4 +248,80 @@ 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
+ });
305
+ });
306
+ // El remedio tiene que ser ejecutable, no un codigo.
307
+ //
308
+ // `doctor` emitia `open-hooks-trust` y nada mas — ni en la referencia del CLI, ni en la
309
+ // salida, ni en la doc. Una corrida real leyo `→ open-hooks-trust` y no tenia que hacer.
310
+ // El texto que se muestra ahora es el que Codex 0.146.0 emite de verdad, observado en una
311
+ // corrida, no una parafrasis. Ver D-010.
312
+ describe('pending-trust tells the user what to actually do', () => {
313
+ it('names the exact prompt Codex shows and the option that grants trust', () => {
314
+ const { pendingTrustGuidance } = require('../../../src/commands/hooks/trust-guidance');
315
+ const text = pendingTrustGuidance('codex').join('\n');
316
+ expect(text).toContain('Hooks need review');
317
+ expect(text).toContain('Trust all and continue');
318
+ });
319
+ it('falls back to a generic message for an agent whose prompt we have not observed', () => {
320
+ // No se inventa el texto de un prompt que nadie vio: seria peor que uno generico,
321
+ // porque mandaria a buscar algo que quiza no existe.
322
+ const { pendingTrustGuidance } = require('../../../src/commands/hooks/trust-guidance');
323
+ const text = pendingTrustGuidance('claude-code').join('\n');
324
+ expect(text).not.toContain('Hooks need review');
325
+ expect(text).toContain('nunca se lo vio correr');
326
+ });
251
327
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentic-workflow-manager",
3
- "version": "6.0.0",
3
+ "version": "6.1.1",
4
4
  "main": "dist/src/index.js",
5
5
  "bin": {
6
6
  "awm": "./dist/src/index.js"