agentic-workflow-manager 9.11.6 → 9.11.8
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/hooks/codex.js +41 -1
- package/dist/tests/commands/hooks/codex.test.js +84 -0
- package/dist/tests/commands/plan/index.test.js +2 -1
- package/dist/tests/helpers/fixture-cleanup.js +21 -0
- package/dist/tests/helpers/fixture-cleanup.test.js +18 -0
- package/dist/tests/integration/sensor-structured-run.e2e.test.js +2 -1
- package/dist/tests/structural/native-release-artifacts.test.js +3 -5
- package/package.json +3 -3
- package/prebuilds/darwin-arm64/secure_fs.node +0 -0
- package/prebuilds/darwin-x64/secure_fs.node +0 -0
- package/prebuilds/win32-arm64/secure_fs.node +0 -0
- package/prebuilds/win32-x64/secure_fs.node +0 -0
|
@@ -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');
|
|
@@ -17,6 +17,7 @@ const canonical_1 = require("../../../src/core/model-policy/canonical");
|
|
|
17
17
|
const capabilities_1 = require("../../../src/core/model-policy/capabilities");
|
|
18
18
|
const journal_1 = require("../../../src/core/model-policy/journal");
|
|
19
19
|
const types_1 = require("../../../src/core/journal/types");
|
|
20
|
+
const fixture_cleanup_1 = require("../../helpers/fixture-cleanup");
|
|
20
21
|
const stdoutWrite = jest.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
21
22
|
const repositoryRoot = path_1.default.resolve(__dirname, '../../../..');
|
|
22
23
|
const approved = (0, validate_1.validatePlanFile)('docs/plans/2026-09-14-compact-only-bootstrap-plan.md', repositoryRoot);
|
|
@@ -181,7 +182,7 @@ describe('plan validate Commander wiring', () => {
|
|
|
181
182
|
expect(fs_1.default.readFileSync(plan).equals(before)).toBe(true);
|
|
182
183
|
}
|
|
183
184
|
finally {
|
|
184
|
-
|
|
185
|
+
(0, fixture_cleanup_1.removeFixtureTree)(fixtureRoot);
|
|
185
186
|
}
|
|
186
187
|
});
|
|
187
188
|
it.each([
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.removeFixtureTree = removeFixtureTree;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const REMOVE_OPTIONS = {
|
|
10
|
+
recursive: true,
|
|
11
|
+
force: true,
|
|
12
|
+
maxRetries: 10,
|
|
13
|
+
retryDelay: 50,
|
|
14
|
+
};
|
|
15
|
+
/** Removes an isolated test fixture while tolerating short-lived Windows locks. */
|
|
16
|
+
function removeFixtureTree(target, remove = fs_1.default.rmSync) {
|
|
17
|
+
if (typeof target !== 'string' || !path_1.default.isAbsolute(target)) {
|
|
18
|
+
throw new Error('fixture cleanup requires an absolute path');
|
|
19
|
+
}
|
|
20
|
+
remove(target, REMOVE_OPTIONS);
|
|
21
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const fixture_cleanup_1 = require("./fixture-cleanup");
|
|
4
|
+
describe('removeFixtureTree', () => {
|
|
5
|
+
it('uses bounded native retries for transient filesystem locks', () => {
|
|
6
|
+
const remove = jest.fn();
|
|
7
|
+
(0, fixture_cleanup_1.removeFixtureTree)('/tmp/fixture', remove);
|
|
8
|
+
expect(remove).toHaveBeenCalledWith('/tmp/fixture', {
|
|
9
|
+
recursive: true,
|
|
10
|
+
force: true,
|
|
11
|
+
maxRetries: 10,
|
|
12
|
+
retryDelay: 50,
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
it.each(['', 'relative-fixture'])('rejects an unsafe fixture path %j', target => {
|
|
16
|
+
expect(() => (0, fixture_cleanup_1.removeFixtureTree)(target)).toThrow('fixture cleanup requires an absolute path');
|
|
17
|
+
});
|
|
18
|
+
});
|
|
@@ -7,6 +7,7 @@ const child_process_1 = require("child_process");
|
|
|
7
7
|
const fs_1 = __importDefault(require("fs"));
|
|
8
8
|
const os_1 = __importDefault(require("os"));
|
|
9
9
|
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const fixture_cleanup_1 = require("../helpers/fixture-cleanup");
|
|
10
11
|
const cliDir = path_1.default.resolve(__dirname, '../..');
|
|
11
12
|
const bin = path_1.default.join(cliDir, 'dist/src/index.js');
|
|
12
13
|
function writeJson(file, value) {
|
|
@@ -126,6 +127,6 @@ test('compiled sensors run materializes a v2 registry command and passes its lit
|
|
|
126
127
|
expect(fs_1.default.existsSync(path_1.default.join(fixture.project, 'structured-shell-injection'))).toBe(false);
|
|
127
128
|
}
|
|
128
129
|
finally {
|
|
129
|
-
|
|
130
|
+
(0, fixture_cleanup_1.removeFixtureTree)(fixture.root);
|
|
130
131
|
}
|
|
131
132
|
});
|
|
@@ -9,14 +9,12 @@ const path_1 = __importDefault(require("path"));
|
|
|
9
9
|
const CLI_ROOT = path_1.default.resolve(__dirname, '..', '..');
|
|
10
10
|
const REPO_ROOT = path_1.default.resolve(CLI_ROOT, '..');
|
|
11
11
|
describe('native release artifacts', () => {
|
|
12
|
-
it('
|
|
12
|
+
it('runs each platform suite once without weakening publication', () => {
|
|
13
13
|
for (const name of ['ci.yml', 'release.yml']) {
|
|
14
14
|
const workflow = fs_1.default.readFileSync(path_1.default.join(REPO_ROOT, '.github', 'workflows', name), 'utf8');
|
|
15
|
-
expect(workflow).toContain('- name: Windows admission smoke');
|
|
16
|
-
expect(workflow).toContain(
|
|
17
|
-
expect(workflow).toContain('run: npx jest tests/commands/plan/index.test.ts --runInBand --bail');
|
|
15
|
+
expect(workflow).not.toContain('- name: Windows admission smoke');
|
|
16
|
+
expect(workflow).not.toContain('run: npx jest tests/commands/plan/index.test.ts --runInBand --bail');
|
|
18
17
|
expect(workflow).toContain('run: npx jest --runInBand --bail');
|
|
19
|
-
expect(workflow.indexOf('- name: Windows admission smoke')).toBeLessThan(workflow.indexOf('run: npx jest --runInBand --bail'));
|
|
20
18
|
}
|
|
21
19
|
const release = fs_1.default.readFileSync(path_1.default.join(REPO_ROOT, '.github', 'workflows', 'release.yml'), 'utf8');
|
|
22
20
|
expect(release).toMatch(/release:\s*\n\s*needs: test/);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentic-workflow-manager",
|
|
3
|
-
"version": "9.11.
|
|
3
|
+
"version": "9.11.8",
|
|
4
4
|
"main": "dist/src/index.js",
|
|
5
5
|
"bin": {
|
|
6
6
|
"awm": "./dist/src/index.js"
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"commander": "^14.0.3",
|
|
53
53
|
"picocolors": "^1.1.1",
|
|
54
54
|
"semver": "^7.7.4",
|
|
55
|
-
"simple-git": "
|
|
55
|
+
"simple-git": "3.36.0"
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
58
58
|
"@types/jest": "^30.0.0",
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
"dependency-cruiser": "16.10.4",
|
|
65
65
|
"eslint": "10.8.1",
|
|
66
66
|
"jest": "^30.2.0",
|
|
67
|
-
"js-yaml": "4.
|
|
67
|
+
"js-yaml": "4.3.2",
|
|
68
68
|
"node-gyp": "^11.5.0",
|
|
69
69
|
"ts-jest": "^29.4.6",
|
|
70
70
|
"ts-node": "^10.9.2",
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|