agentic-workflow-manager 6.4.0 → 6.4.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.
@@ -0,0 +1,145 @@
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
+ // El camino `awm track join` -> fase `JOIN_REQUESTED`, que NO EXISTÍA.
7
+ //
8
+ // `applyRequestToState` era una cadena de `if (env.kind === …) { …; return; }` sin rama para
9
+ // `track-join-request`: se caía por el final sin lanzar, el caller no veía excepción, hacía
10
+ // `applied++` y BORRABA el archivo. El comando salía 0 con su requestId, el journal contaba
11
+ // la request como aplicada, y el track se quedaba en `ACTIVE` para siempre — como C3 exige la
12
+ // cohorte entera congelada antes de mergear, jamás llegaba a `MERGED_UNVERIFIED` ni a
13
+ // `COMPLETE`. Confirmación cruzada en su momento: `join-requested` era la ÚNICA observación
14
+ // del protocolo con cero productores en todo `src/`.
15
+ //
16
+ // Estos tests cubren las tres propiedades del camino reparado:
17
+ // 1. un join sobre un track `ACTIVE` mueve la fase (vía el reducer, no acá);
18
+ // 2. un join que llega ANTES de la activación ESPERA en vez de perderse;
19
+ // 3. un join que ya no puede aplicarse nunca se limpia, sin girar en cada tick.
20
+ //
21
+ // (2) es un bug encontrado en la certificación con supervisor vivo, sobre la PRIMERA versión
22
+ // de este fix: con `maxParallel` chico la cohorte activa de a un track por vez, así que el
23
+ // controller termina su trabajo y pide el join mientras su track sigue en `ARMED`. Descartar
24
+ // el pedido ahí reintroduce exactamente el modo de falla que este cambio vino a cerrar — un
25
+ // pedido que reporta éxito y no hace nada — y solo pasaba desapercibido porque el controller
26
+ // scripteado re-emite el join en cada vuelta.
27
+ const fs_1 = __importDefault(require("fs"));
28
+ const path_1 = __importDefault(require("path"));
29
+ const git_fixture_1 = require("../../helpers/git-fixture");
30
+ const tracks_1 = require("../../../src/commands/watch/tracks");
31
+ const apply_1 = require("../../../src/commands/watch/apply");
32
+ const emit_1 = require("../../../src/commands/track/emit");
33
+ const store_1 = require("../../../src/core/journal/store");
34
+ const paths_1 = require("../../../src/core/journal/paths");
35
+ const BRANCH = 'main';
36
+ describe('awm track join -> JOIN_REQUESTED (R6.1)', () => {
37
+ let planRoot;
38
+ let baseSha;
39
+ beforeEach(() => {
40
+ planRoot = (0, git_fixture_1.initRepo)();
41
+ (0, git_fixture_1.commitFile)(planRoot, '.gitignore', '.awm/\n');
42
+ baseSha = (0, git_fixture_1.commitFile)(planRoot, 'seed.txt', 'seed');
43
+ (0, store_1.initJournal)(planRoot, BRANCH);
44
+ });
45
+ afterEach(() => { fs_1.default.rmSync(planRoot, { recursive: true, force: true }); });
46
+ function declareCohort(phaseA, phaseB = 'ACTIVE') {
47
+ const s = (0, store_1.readJournal)(planRoot, BRANCH).state;
48
+ s.cohortPhase = 'ACTIVE';
49
+ s.cohortBaseSha = baseSha;
50
+ // `assertProtocolInvariants` exige `frozenHeadSha` en todo track congelado o
51
+ // posterior: la fixture lo provee en vez de bajarle el estándar al invariante.
52
+ const frozen = ['FROZEN', 'JOIN_INTENT', 'MERGED_UNVERIFIED', 'JOINED'];
53
+ s.tracks = [
54
+ {
55
+ trackId: 'a', worktreePath: path_1.default.join(planRoot, '../nope-a'), branch: 'awm-track/a',
56
+ ownership: [], sharedResources: [], dependsOn: [],
57
+ fencingToken: 'fence-a'.padEnd(32, '0'), phase: phaseA, readinessNonce: 'ready-a'.padEnd(32, '0'),
58
+ frozenHeadSha: frozen.includes(phaseA) ? baseSha : undefined,
59
+ },
60
+ {
61
+ trackId: 'b', worktreePath: path_1.default.join(planRoot, '../nope-b'), branch: 'awm-track/b',
62
+ ownership: [], sharedResources: [], dependsOn: [],
63
+ fencingToken: 'fence-b'.padEnd(32, '0'), phase: phaseB, readinessNonce: 'ready-b'.padEnd(32, '0'),
64
+ },
65
+ ];
66
+ (0, store_1.writeJournal)(planRoot, BRANCH, s);
67
+ return (0, store_1.readJournal)(planRoot, BRANCH).state;
68
+ }
69
+ /** Runtime que explota ante cualquier efecto real: estos tests miden SOLO la producción
70
+ * de la observación de join, nunca un freeze/merge de verdad. */
71
+ function inertRuntime() {
72
+ const real = (0, tracks_1.defaultTrackRuntime)(planRoot, BRANCH);
73
+ return {
74
+ ...real,
75
+ addWorktree: () => { throw new Error('no debería llamarse'); },
76
+ initTrackJournal: () => { throw new Error('no debería llamarse'); },
77
+ spawnSupervisor: () => { throw new Error('no debería llamarse'); },
78
+ observeSupervisor: () => ({ kind: 'absent' }),
79
+ };
80
+ }
81
+ function events() {
82
+ let raw = '';
83
+ try {
84
+ raw = fs_1.default.readFileSync((0, paths_1.eventsPath)(planRoot, BRANCH), 'utf8');
85
+ }
86
+ catch {
87
+ return [];
88
+ }
89
+ return raw.split('\n').filter((l) => l.trim().length > 0).map((l) => JSON.parse(l));
90
+ }
91
+ function phaseOf(s, trackId) {
92
+ return s.tracks?.find((t) => t.trackId === trackId)?.phase;
93
+ }
94
+ function joinFlagOf(s, trackId) {
95
+ return s.tracks?.find((t) => t.trackId === trackId)?.joinRequested;
96
+ }
97
+ it('el join de un track ACTIVE llega hasta la fase, pasando por la request real del CLI', async () => {
98
+ declareCohort('ACTIVE');
99
+ // La request se emite con el MISMO emisor que usa `awm track join`, no a mano: lo que
100
+ // estaba roto era justamente el tramo entre ese emisor y el estado.
101
+ (0, emit_1.emitTrackRequest)(planRoot, BRANCH, 'g1', 'track-join-request', 'a');
102
+ expect((0, apply_1.consumePendingRequests)(planRoot, BRANCH, 'g1').applied).toBe(1);
103
+ const s1 = (0, store_1.readJournal)(planRoot, BRANCH).state;
104
+ expect(joinFlagOf(s1, 'a')).toBe(true);
105
+ expect(phaseOf(s1, 'a')).toBe('ACTIVE'); // el consumo transaccional NO mueve fases
106
+ const out = await (0, tracks_1.reconcileTracks)(planRoot, BRANCH, s1, inertRuntime(), 2);
107
+ expect(phaseOf(out.state, 'a')).toBe('JOIN_REQUESTED');
108
+ expect(joinFlagOf(out.state, 'a')).toBeUndefined();
109
+ expect(events().some((e) => e.kind === 'track-join-observed' && e.trackId === 'a')).toBe(true);
110
+ });
111
+ it('un join que llega ANTES de la activación espera, y se aplica cuando el track activa', async () => {
112
+ // Carrera real de la certificación: `maxParallel` 1 activa de a un track, así que el
113
+ // controller pide el join de `a` mientras `a` sigue en ARMED.
114
+ declareCohort('ARMED', 'ACTIVE');
115
+ (0, emit_1.emitTrackRequest)(planRoot, BRANCH, 'g1', 'track-join-request', 'a');
116
+ (0, apply_1.consumePendingRequests)(planRoot, BRANCH, 'g1');
117
+ let s = (0, store_1.readJournal)(planRoot, BRANCH).state;
118
+ s = (await (0, tracks_1.reconcileTracks)(planRoot, BRANCH, s, inertRuntime(), 1)).state;
119
+ // Sigue pendiente: no se descartó por haber llegado temprano.
120
+ expect(joinFlagOf(s, 'a')).toBe(true);
121
+ expect(phaseOf(s, 'a')).toBe('ARMED');
122
+ // El track activa (lo hace el protocolo cuando hay cupo) y recién ahí el join aplica.
123
+ const activated = (0, store_1.readJournal)(planRoot, BRANCH).state;
124
+ for (const t of activated.tracks ?? [])
125
+ if (t.trackId === 'a')
126
+ t.phase = 'ACTIVE';
127
+ (0, store_1.writeJournal)(planRoot, BRANCH, activated);
128
+ const out = await (0, tracks_1.reconcileTracks)(planRoot, BRANCH, (0, store_1.readJournal)(planRoot, BRANCH).state, inertRuntime(), 2);
129
+ expect(phaseOf(out.state, 'a')).toBe('JOIN_REQUESTED');
130
+ expect(joinFlagOf(out.state, 'a')).toBeUndefined();
131
+ });
132
+ it('un join que ya no puede aplicarse nunca se limpia y no gira en cada tick', async () => {
133
+ // `a` ya está mergeado: un join re-emitido por un controller relevado es moot.
134
+ declareCohort('MERGED_UNVERIFIED', 'ACTIVE');
135
+ (0, emit_1.emitTrackRequest)(planRoot, BRANCH, 'g1', 'track-join-request', 'a');
136
+ (0, apply_1.consumePendingRequests)(planRoot, BRANCH, 'g1');
137
+ const out = await (0, tracks_1.reconcileTracks)(planRoot, BRANCH, (0, store_1.readJournal)(planRoot, BRANCH).state, inertRuntime(), 2);
138
+ expect(joinFlagOf(out.state, 'a')).toBeUndefined();
139
+ expect(phaseOf(out.state, 'a')).toBe('MERGED_UNVERIFIED');
140
+ expect(events().filter((e) => e.kind === 'track-join-moot')).toHaveLength(1);
141
+ // Un segundo pase no vuelve a emitir nada: la marca ya no está.
142
+ await (0, tracks_1.reconcileTracks)(planRoot, BRANCH, (0, store_1.readJournal)(planRoot, BRANCH).state, inertRuntime(), 2);
143
+ expect(events().filter((e) => e.kind === 'track-join-moot')).toHaveLength(1);
144
+ });
145
+ });
@@ -10,6 +10,47 @@ describe('assertProviderSupported', () => {
10
10
  });
11
11
  expect(exec).toHaveBeenCalledWith('codex', ['--version'], expect.any(Object));
12
12
  });
13
+ // Regression: npm installs `codex` as `codex.cmd` on Windows, and
14
+ // execFileSync can't CreateProcess a `.cmd` shim without a shell — it threw
15
+ // ENOENT here even on a machine where `codex --version` worked fine typed
16
+ // directly. `provider.versionCommand` is hardcoded first-party config
17
+ // (providers/index.ts), never attacker-controlled, so `shell: true` here
18
+ // carries none of the injection risk core/paths.ts's resolveOnPath was
19
+ // built to avoid for sensors.json's user/registry-supplied `cmd`.
20
+ describe('on native Windows', () => {
21
+ const realPlatform = process.platform;
22
+ afterEach(() => {
23
+ Object.defineProperty(process, 'platform', { value: realPlatform, configurable: true });
24
+ });
25
+ it('runs the version probe through a shell so the .cmd shim resolves', () => {
26
+ Object.defineProperty(process, 'platform', { value: 'win32', configurable: true });
27
+ const exec = jest.fn(() => Buffer.from('codex-cli 0.145.0\n'));
28
+ (0, provider_version_1.assertProviderSupported)('codex', exec);
29
+ expect(exec).toHaveBeenCalledWith('codex', ['--version'], expect.objectContaining({ shell: true }));
30
+ });
31
+ it('does not use a shell on non-Windows platforms', () => {
32
+ Object.defineProperty(process, 'platform', { value: 'darwin', configurable: true });
33
+ const exec = jest.fn(() => Buffer.from('codex-cli 0.145.0\n'));
34
+ (0, provider_version_1.assertProviderSupported)('codex', exec);
35
+ expect(exec).toHaveBeenCalledWith('codex', ['--version'], expect.objectContaining({ shell: false }));
36
+ });
37
+ // Regression from the fix above: shipping shell:true changed how a
38
+ // GENUINELY missing binary fails. Without a shell it's a spawn-level
39
+ // ENOENT; through cmd.exe the shell itself starts fine and the missing
40
+ // command surfaces as a non-zero exit with this exact stderr text — no
41
+ // ENOENT anywhere. windows-latest CI (no codex installed) caught this:
42
+ // it started reporting "version probe failed" instead of "not
43
+ // installed" the first time shell:true shipped without this branch.
44
+ it('still reports "not installed" when the shell itself says the command is unknown', () => {
45
+ Object.defineProperty(process, 'platform', { value: 'win32', configurable: true });
46
+ const shellNotFound = Object.assign(new Error('Command failed: codex --version'), {
47
+ status: 1,
48
+ stderr: Buffer.from("'codex' is not recognized as an internal or external command,\r\noperable program or batch file.\r\n"),
49
+ });
50
+ expect(() => (0, provider_version_1.assertProviderSupported)('codex', () => { throw shellNotFound; }))
51
+ .toThrow('Codex is not installed or not available on PATH');
52
+ });
53
+ });
13
54
  it.each(['0.145.1', '0.146.0', '1.0.0'])('accepts stable Codex version %s above the minimum', (version) => {
14
55
  expect((0, provider_version_1.assertProviderSupported)('codex', () => Buffer.from(`codex-cli ${version}\n`))).toEqual({ provider: 'codex', version });
15
56
  });
@@ -91,7 +91,11 @@ describe('update-check', () => {
91
91
  const m = require('../../src/core/update-check');
92
92
  const runner = jest.fn().mockReturnValue({ status: 1 });
93
93
  const warn = jest.spyOn(console, 'warn').mockImplementation(() => { });
94
- await m.offerSelfUpdate({ current: '2.0.0', latest: '2.1.0', confirmImpl: async () => true, runner });
94
+ // `mode: 'prompt'` explícito: la premisa de este test es que HAY un humano
95
+ // confirmando. Bajo jest stdin no es un TTY, y el modo por defecto pasó a ser
96
+ // `skip` justamente para que `awm update` no se cuelgue donde no hay a quién
97
+ // preguntarle — sin declarar el modo, este test mediría el camino desatendido.
98
+ await m.offerSelfUpdate({ current: '2.0.0', latest: '2.1.0', mode: 'prompt', confirmImpl: async () => true, runner });
95
99
  expect(runner).toHaveBeenCalled();
96
100
  expect(warn.mock.calls.flat().join('\n')).toContain('npm i -g agentic-workflow-manager');
97
101
  warn.mockRestore();
@@ -6,6 +6,27 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  const fs_1 = __importDefault(require("fs"));
7
7
  const os_1 = __importDefault(require("os"));
8
8
  const path_1 = __importDefault(require("path"));
9
+ const config_1 = require("../../src/utils/config");
10
+ // Regression for the `awm remove <bundle> --yes` gap found running the issue #55
11
+ // Windows playbook (CORE-17): `--yes` skipped the agent prompt but not the scope
12
+ // one, so a supposedly non-interactive removal still hung waiting on a picker.
13
+ // D-006's own stated rule is "`--yes` implica cero prompts" — this closes the one
14
+ // call site that had drifted from it.
15
+ describe('resolveScopeOption', () => {
16
+ it('falls back to the default when nothing explicit was passed (the --yes path)', () => {
17
+ expect((0, config_1.resolveScopeOption)(undefined, 'local')).toEqual({ ok: true, scope: 'local' });
18
+ expect((0, config_1.resolveScopeOption)(undefined, 'global')).toEqual({ ok: true, scope: 'global' });
19
+ });
20
+ it('an explicit valid value wins over the default', () => {
21
+ expect((0, config_1.resolveScopeOption)('global', 'local')).toEqual({ ok: true, scope: 'global' });
22
+ });
23
+ it('rejects a value that is neither local nor global', () => {
24
+ expect((0, config_1.resolveScopeOption)('bogus', 'local')).toEqual({
25
+ ok: false,
26
+ error: 'Invalid scope "bogus". Use: local or global.',
27
+ });
28
+ });
29
+ });
9
30
  describe('Preferences Manager', () => {
10
31
  let tmpHome;
11
32
  let tmpWork;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentic-workflow-manager",
3
- "version": "6.4.0",
3
+ "version": "6.4.2",
4
4
  "main": "dist/src/index.js",
5
5
  "bin": {
6
6
  "awm": "./dist/src/index.js"