agentic-workflow-manager 6.1.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.
- package/dist/src/commands/context-budget/budget.js +13 -0
- package/dist/src/commands/context-budget/index.js +8 -0
- package/dist/src/commands/hooks/claude.js +9 -2
- package/dist/src/commands/hooks/codex.js +10 -2
- package/dist/src/commands/hooks/shared.js +42 -0
- package/dist/tests/commands/context-budget/budget.test.js +36 -0
- package/dist/tests/commands/hooks/codex.test.js +54 -0
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
+
}
|
|
@@ -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
|
//
|