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.
- 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/src/core/diagnostics/provider-checks.js +50 -25
- package/dist/src/core/renderers/registry.js +26 -0
- package/dist/tests/commands/context-budget/budget.test.js +36 -0
- package/dist/tests/commands/hooks/codex.test.js +54 -0
- package/dist/tests/core/diagnostics/provider-tier.test.js +22 -2
- 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
|
+
}
|
|
@@ -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`
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
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
|
|
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:
|
|
112
|
+
state: corrupt.length > 0 ? 'broken' : 'supported',
|
|
104
113
|
target: dir,
|
|
105
114
|
owners: owners.length > 1 ? owners : undefined,
|
|
106
|
-
detail:
|
|
107
|
-
|
|
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
|
-
/**
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
|
|
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
|
-
})
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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', () => {
|