agentic-workflow-manager 3.13.1 → 3.13.3

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,14 @@ function providerCheckLine(check) {
99
99
  function renderProviderReport(report) {
100
100
  const lines = [];
101
101
  lines.push(picocolors_1.default.bold('AWM · harness status'));
102
+ lines.push(picocolors_1.default.dim(` platform: ${(0, paths_1.platformLabel)()}`));
103
+ // Advisory only, native Windows only. This is the text rendered by the
104
+ // real `awm doctor` command (`runDoctor` below) — the diagnostics surface
105
+ // an operator actually goes looking at for platform-specific detail.
106
+ // `init`/`update`/`sync` also note it once each, at the top of their own
107
+ // run (`runInit`/`runUpdateCore`/`runSyncCore`), independently of this.
108
+ if ((0, paths_1.isWindowsNative)())
109
+ lines.push(picocolors_1.default.dim(` ${paths_1.WINDOWS_KNOWN_GAP.replace(/\n\s*/g, ' ')}`));
102
110
  lines.push('');
103
111
  for (const provider of report.providers) {
104
112
  lines.push(`Provider: ${provider.label} ${picocolors_1.default.dim(`(${provider.tier})`)}`);
@@ -125,6 +125,11 @@ function reportInitFailure(o) {
125
125
  return 2;
126
126
  }
127
127
  async function runInit(opts = {}) {
128
+ // Fires at most once per `awm init` run (native Windows only) — this is
129
+ // the single emission point; `renderReport`/`renderInitOutcome` below do
130
+ // NOT also embed it (that used to triple-fire the same text: once here,
131
+ // once in the "Initial state" render, once in "Final state").
132
+ (0, paths_1.noteWindowsCaveat)((m) => console.log(picocolors_1.default.dim(`ℹ ${m}`)));
128
133
  const cwd = opts.cwd ?? process.cwd();
129
134
  const agent = opts.agent === undefined ? 'claude-code' : (0, providers_1.requireAgentTarget)(opts.agent);
130
135
  // R2: gate BEFORE anything is read or written — an unsupported provider
@@ -313,7 +318,6 @@ function registerInitCommand(program) {
313
318
  .option('--machine-only', 'Only run machine-level steps (skip project steps)')
314
319
  .option('--json', 'Emit the InitOutcome as JSON — on success and on failure (failed steps + rollback)')
315
320
  .action(async (options) => {
316
- (0, paths_1.warnIfUnsupportedPlatform)((m) => console.warn(picocolors_1.default.yellow(`⚠ ${m}`)));
317
321
  const code = await runInit({
318
322
  yes: options.yes,
319
323
  agent: options.agent,
@@ -16,6 +16,7 @@ exports.runSyncCore = runSyncCore;
16
16
  // intro/outro chrome lives in `index.ts`'s Commander registration instead
17
17
  // (which already imports clack at the top and is never `require()`d by tests).
18
18
  const picocolors_1 = __importDefault(require("picocolors"));
19
+ const paths_1 = require("../core/paths");
19
20
  const profile_1 = require("../core/profile");
20
21
  const registries_1 = require("../core/registries");
21
22
  const bundles_1 = require("../core/bundles");
@@ -31,6 +32,9 @@ const defaultDeps = {
31
32
  };
32
33
  /** Core, UI-free `awm sync` logic — see `runSync` for the Commander-facing wrapper. */
33
34
  async function runSyncCore(options, deps = {}) {
35
+ // Fires at most once per `awm sync` run, native Windows only — the single
36
+ // emission point for this command (mirrors `runInit`/`runUpdateCore`).
37
+ (0, paths_1.noteWindowsCaveat)((m) => console.log(picocolors_1.default.dim(`ℹ ${m}`)));
34
38
  const d = { ...defaultDeps, ...deps };
35
39
  const cwd = options.cwd ?? process.cwd();
36
40
  const projectRoot = (0, profile_1.findProjectRoot)(cwd);
@@ -20,6 +20,7 @@ exports.runUpdateCore = runUpdateCore;
20
20
  // instead (which already imports clack at the top and is never `require()`d
21
21
  // by tests).
22
22
  const picocolors_1 = __importDefault(require("picocolors"));
23
+ const paths_1 = require("../core/paths");
23
24
  const config_1 = require("../utils/config");
24
25
  const agent_targets_1 = require("../core/agent-targets");
25
26
  const registries_1 = require("../core/registries");
@@ -49,6 +50,9 @@ const defaultDeps = {
49
50
  * `runInit`'s `actions` seam).
50
51
  */
51
52
  async function runUpdateCore(options = {}, deps = {}) {
53
+ // Fires at most once per `awm update` run, native Windows only — the
54
+ // single emission point for this command (mirrors `runInit`/`runSyncCore`).
55
+ (0, paths_1.noteWindowsCaveat)((m) => console.log(picocolors_1.default.dim(`ℹ ${m}`)));
52
56
  const d = { ...defaultDeps, ...deps };
53
57
  const prefs = (0, config_1.getPreferences)();
54
58
  const resolved = (0, agent_targets_1.resolveAgentTargetsOrError)({ prefs, explicit: options.agent });
@@ -275,17 +275,51 @@ function spawnStructured(argv, cwd, nonce, extraEnv = {}) {
275
275
  * rompia el invariante "JAMAS safe sin evidencia" (safeToReplace
276
276
  * devolvia 'safe' para un proceso vivo). process.kill(pid,0) evita la
277
277
  * capa de emulacion por completo. */
278
+ /** Sleep sincronico REAL, multiplataforma, sin depender de un binario externo
279
+ * (`sleep` no existe nativamente en win32 — ver el comentario de sleepSync
280
+ * mas arriba). `Atomics.wait` sobre un buffer compartido bloquea el hilo
281
+ * actual durante `ms` sin abrir ningun subproceso; funciona identico en
282
+ * cualquier plataforma que soporte Node. Usado SOLO por pidExistsNative
283
+ * para el reintento acotado de abajo — nunca en un hot-path de alta
284
+ * frecuencia. */
285
+ function sleepMsSync(ms) {
286
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
287
+ }
288
+ /** Reintento acotado (R6, post-mortem de CI real): dos corridas reales de
289
+ * windows-latest sobre el MISMO commit (uno vía `release.yml`, uno vía
290
+ * `ci.yml`, ambos re-corriendo el suite completo desde cero) dieron
291
+ * resultados DISTINTOS para el mismo test — `pidExistsNative` reportando
292
+ * ESRCH para un proceso recien spawneado por el propio test, genuinamente
293
+ * vivo — evidencia directa de una condicion de carrera transitoria
294
+ * especifica de esta plataforma/runner, no un bug determinista (el codigo
295
+ * no cambio entre ambas corridas). Un solo intento en el hot-path exacto
296
+ * "acabo de spawnear esto, ¿esta vivo?" no le da tiempo al SO a que el pid
297
+ * sea consultable via OpenProcess bajo carga pesada del runner. Reintentar
298
+ * con una pausa breve ANTES de declarar ESRCH definitivo no debilita el
299
+ * invariante "jamas muerte sin evidencia" — un proceso genuinamente muerto
300
+ * sigue reportando ESRCH en el reintento; esto solo absorbe el falso
301
+ * negativo transitorio. Costo maximo: ~150ms, y SOLO en la rama que ya iba
302
+ * a declarar "no existe" — el camino feliz (proceso vivo, exito inmediato)
303
+ * no paga nada. */
304
+ const PID_EXISTS_RETRY_ATTEMPTS = 3;
305
+ const PID_EXISTS_RETRY_DELAY_MS = 50;
278
306
  function pidExistsNative(pid) {
279
- try {
280
- process.kill(pid, 0);
281
- return true;
282
- }
283
- catch (error) {
284
- // ESRCH = el SO confirmo que el pid no existe. Cualquier otro
285
- // codigo (ej. EPERM: existe pero sin permiso de senializarlo) NO
286
- // es evidencia de muerte falla a favor de "vivo" (R2.1).
287
- return error.code !== 'ESRCH';
307
+ for (let attempt = 0; attempt < PID_EXISTS_RETRY_ATTEMPTS; attempt++) {
308
+ try {
309
+ process.kill(pid, 0);
310
+ return true;
311
+ }
312
+ catch (error) {
313
+ // ESRCH = el SO confirmo que el pid no existe. Cualquier otro
314
+ // codigo (ej. EPERM: existe pero sin permiso de senializarlo) NO
315
+ // es evidencia de muerte — falla a favor de "vivo" (R2.1).
316
+ if (error.code !== 'ESRCH')
317
+ return true;
318
+ if (attempt < PID_EXISTS_RETRY_ATTEMPTS - 1)
319
+ sleepMsSync(PID_EXISTS_RETRY_DELAY_MS);
320
+ }
288
321
  }
322
+ return false;
289
323
  }
290
324
  /** Vivo Y con la MISMA identidad — tupla completa, nunca PID solo (R2.1,
291
325
  * bloqueador 6): startTime + pgid + digest de ps args.
@@ -3,13 +3,13 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.WINDOWS_NATIVE_WARNING = void 0;
6
+ exports.WINDOWS_KNOWN_GAP = void 0;
7
7
  exports.homeDir = homeDir;
8
8
  exports.awmHome = awmHome;
9
9
  exports.platform = platform;
10
10
  exports.isWindowsNative = isWindowsNative;
11
11
  exports.platformLabel = platformLabel;
12
- exports.warnIfUnsupportedPlatform = warnIfUnsupportedPlatform;
12
+ exports.noteWindowsCaveat = noteWindowsCaveat;
13
13
  exports.resolveOnPath = resolveOnPath;
14
14
  // cli/src/core/paths.ts
15
15
  //
@@ -35,11 +35,14 @@ function platform() {
35
35
  function isWindowsNative() {
36
36
  return platform() === 'win32';
37
37
  }
38
- /** Human-friendly platform label for diagnostics. */
38
+ /** Human-friendly platform label for diagnostics. Windows is a first-class,
39
+ * CI-verified platform since R6 (`.github/workflows/ci.yml` runs the full
40
+ * suite on `ubuntu-latest` + `windows-latest` on every PR) — this no longer
41
+ * hedges toward WSL. */
39
42
  function platformLabel() {
40
43
  switch (platform()) {
41
44
  case 'win32':
42
- return 'Windows (native — not supported yet, use WSL)';
45
+ return 'Windows (native, CI-verified)';
43
46
  case 'darwin':
44
47
  return 'macOS';
45
48
  case 'linux':
@@ -48,13 +51,28 @@ function platformLabel() {
48
51
  return platform();
49
52
  }
50
53
  }
51
- exports.WINDOWS_NATIVE_WARNING = 'AWM detected native Windows. Native support is deferred; the recommended path today is WSL.\n' +
52
- ' Install WSL (https://learn.microsoft.com/windows/wsl/install) and run AWM inside your Linux distro.\n' +
53
- ' Continuing in best-effort mode, but some steps (symlinks, hooks) may not work.';
54
- /** Emit the unsupported-platform warning via the provided logger, only on native Windows. */
55
- function warnIfUnsupportedPlatform(log) {
54
+ /**
55
+ * The one honest, narrow gap left on native Windows: `awm watch`'s supervisor
56
+ * crash-recovery E2E tests (spawn -> identity capture -> adoption after the
57
+ * supervisor is killed) never converged on real `windows-latest` CI despite 4
58
+ * evidence-based fix attempts in R6 (WMI-based `refIsAlive`, `activitySnapshot`
59
+ * degraded off `ps`/`pgrep`, `spawnStructured`'s `detached` flag tried both
60
+ * ways) — scoped POSIX-only (`itPosix`) in `cli/tests/commands/watch/
61
+ * supervisor-loop.test.ts` (2 tests) and `cli/tests/commands/watch/
62
+ * e2e-crash.test.ts` (2 tests); see the `refIsAlive` comment in
63
+ * `cli/src/core/journal/process.ts` for the full investigation. This is
64
+ * deliberately narrow, not a blanket "some things may not work" hedge:
65
+ * `awm init`/`update`/`sync`/`sensors`/`preflight`/`doctor`/hooks are all
66
+ * exercised green by the same CI matrix and are unaffected by this gap.
67
+ */
68
+ exports.WINDOWS_KNOWN_GAP = 'AWM on native Windows: supported and continuously verified in CI (ubuntu-latest + windows-latest, every PR).\n' +
69
+ ' One known, narrow gap: `awm watch`\'s supervisor crash-recovery (spawn -> identity capture -> adoption\n' +
70
+ ' after the supervisor is killed) has not yet converged on real windows-latest CI — see\n' +
71
+ ' cli/src/core/journal/process.ts (refIsAlive) for detail. Everything else is CI-verified on Windows.';
72
+ /** Emit `WINDOWS_KNOWN_GAP` via the provided logger, only on native Windows. */
73
+ function noteWindowsCaveat(log) {
56
74
  if (isWindowsNative())
57
- log(exports.WINDOWS_NATIVE_WARNING);
75
+ log(exports.WINDOWS_KNOWN_GAP);
58
76
  }
59
77
  /** Resolve a binary on PATH portably: `where` on win32, POSIX `command -v` elsewhere. */
60
78
  function resolveOnPath(bin) {
package/dist/src/index.js CHANGED
@@ -42,7 +42,6 @@ const sync_1 = require("./commands/sync");
42
42
  const update_1 = require("./commands/update");
43
43
  const agent_targets_1 = require("./core/agent-targets");
44
44
  const update_check_1 = require("./core/update-check");
45
- const paths_1 = require("./core/paths");
46
45
  const cli_version_1 = require("./core/cli-version");
47
46
  const program = new commander_1.Command();
48
47
  program.name('awm').description('Agentic Workflow Manager').version((0, cli_version_1.cliVersion)());
@@ -390,7 +389,6 @@ program.command('update')
390
389
  .option('-a, --agent <agent>', `Target agent(s), comma-separated: ${providers_1.AGENT_TARGETS.join(', ')} (defaults to every enabled agent)`)
391
390
  .action(async (options) => {
392
391
  (0, prompts_1.intro)(picocolors_1.default.bgCyan(picocolors_1.default.black(' AWM - Update Registries ')));
393
- (0, paths_1.warnIfUnsupportedPlatform)((m) => console.warn(picocolors_1.default.yellow(`⚠ ${m}`)));
394
392
  const result = await (0, update_1.runUpdateCore)(options);
395
393
  (0, prompts_1.outro)(result.code === 0 ? '✅ Registries, skills and hooks updated.' : picocolors_1.default.red('Update failed — see errors above.'));
396
394
  process.exitCode = result.code;
@@ -401,7 +399,6 @@ program.command('sync')
401
399
  .option('-m, --method <method>', 'Install method: symlink or copy', 'symlink')
402
400
  .action(async (options) => {
403
401
  (0, prompts_1.intro)(picocolors_1.default.bgCyan(picocolors_1.default.black(' AWM - Sync Project Profile ')));
404
- (0, paths_1.warnIfUnsupportedPlatform)((m) => console.warn(picocolors_1.default.yellow(`⚠ ${m}`)));
405
402
  const { code } = await (0, sync_1.runSyncCore)(options);
406
403
  (0, prompts_1.outro)(code === 0 ? 'Done.' : picocolors_1.default.red('Sync failed — see errors above.'));
407
404
  process.exitCode = code;
@@ -6,17 +6,61 @@ describe('doctor renderReport — platform line', () => {
6
6
  afterEach(() => {
7
7
  Object.defineProperty(process, 'platform', { value: realPlatform, configurable: true });
8
8
  });
9
+ function setPlatform(p) {
10
+ Object.defineProperty(process, 'platform', { value: p, configurable: true });
11
+ }
9
12
  function emptyReport() {
10
13
  return { overall: 'healthy', hasProject: false, projectName: undefined, results: [] };
11
14
  }
12
15
  it('renders the platform label under the Machine header', () => {
13
- Object.defineProperty(process, 'platform', { value: 'linux', configurable: true });
16
+ setPlatform('linux');
14
17
  const out = (0, doctor_1.renderReport)(emptyReport());
15
18
  expect(out).toContain('platform: Linux');
16
19
  });
17
- it('flags native Windows with a WSL hint', () => {
18
- Object.defineProperty(process, 'platform', { value: 'win32', configurable: true });
20
+ it('labels native Windows as supported and CI-verified, not deferred to WSL', () => {
21
+ setPlatform('win32');
22
+ const out = (0, doctor_1.renderReport)(emptyReport());
23
+ expect(out).toContain('platform: Windows (native, CI-verified)');
24
+ expect(out).not.toContain('WSL');
25
+ });
26
+ // `renderReport` (init's before/after dashboard) deliberately does NOT
27
+ // embed the Windows caveat — it used to (R7), which made a single `awm
28
+ // init` run print the same caveat text 3 times (once via `noteWindowsCaveat`
29
+ // at the top of `runInit`, once for "Initial state", once for "Final
30
+ // state"). The caveat now lives solely in `runInit`'s own single emission
31
+ // and in `renderProviderReport` below (the real `awm doctor` output).
32
+ it('never embeds the awm-watch supervisor caveat in the init before/after dashboard', () => {
33
+ setPlatform('win32');
19
34
  const out = (0, doctor_1.renderReport)(emptyReport());
20
- expect(out).toContain('WSL');
35
+ expect(out).not.toMatch(/awm watch/i);
36
+ });
37
+ });
38
+ // Regression coverage for the confirmed bug: the `WINDOWS_KNOWN_GAP` caveat
39
+ // used to live only inside `renderReport`, whose only callers are `init.ts`'s
40
+ // before/after dashboard — `awm doctor`'s REAL command path
41
+ // (`runDoctor` → `renderProviderReport`) never touched it, so a user running
42
+ // `awm doctor` on native Windows never saw the caveat its own code comment
43
+ // claimed doctor was the place for. This exercises `renderProviderReport`
44
+ // directly, the function `runDoctor` actually calls for its text output.
45
+ describe('doctor renderProviderReport — platform caveat (the real `awm doctor` path)', () => {
46
+ const realPlatform = process.platform;
47
+ afterEach(() => {
48
+ Object.defineProperty(process, 'platform', { value: realPlatform, configurable: true });
49
+ });
50
+ function setPlatform(p) {
51
+ Object.defineProperty(process, 'platform', { value: p, configurable: true });
52
+ }
53
+ function emptyProviderReport() {
54
+ return { overall: 'healthy', providers: [] };
55
+ }
56
+ it('surfaces the narrow awm-watch supervisor caveat on native Windows only, not on macOS or Linux', () => {
57
+ setPlatform('win32');
58
+ const winOut = (0, doctor_1.renderProviderReport)(emptyProviderReport());
59
+ expect(winOut).toMatch(/awm watch/i);
60
+ for (const p of ['linux', 'darwin']) {
61
+ setPlatform(p);
62
+ const out = (0, doctor_1.renderProviderReport)(emptyProviderReport());
63
+ expect(out).not.toMatch(/awm watch/i);
64
+ }
21
65
  });
22
66
  });
@@ -121,6 +121,25 @@ describe('runDoctor', () => {
121
121
  const code = (0, doctor_1.runDoctor)({ cwd: tmpHome });
122
122
  expect(code).toBe(1);
123
123
  });
124
+ // Finding #1 (R7 QA): the real `awm doctor` command (this `runDoctor`,
125
+ // text mode — not `renderReport`, which only `init.ts` calls) must
126
+ // actually surface the Windows caveat on native Windows.
127
+ it('surfaces the Windows caveat in real text output on native Windows only', () => {
128
+ const realPlatform = process.platform;
129
+ try {
130
+ Object.defineProperty(process, 'platform', { value: 'win32', configurable: true });
131
+ writeSpy.mockClear();
132
+ (0, doctor_1.runDoctor)({ cwd: tmpHome });
133
+ expect(stdout()).toMatch(/awm watch/i);
134
+ Object.defineProperty(process, 'platform', { value: 'linux', configurable: true });
135
+ writeSpy.mockClear();
136
+ (0, doctor_1.runDoctor)({ cwd: tmpHome });
137
+ expect(stdout()).not.toMatch(/awm watch/i);
138
+ }
139
+ finally {
140
+ Object.defineProperty(process, 'platform', { value: realPlatform, configurable: true });
141
+ }
142
+ });
124
143
  it('--json emits a parseable provider report and keeps the same exit code', () => {
125
144
  const code = (0, doctor_1.runDoctor)({ cwd: tmpHome, json: true });
126
145
  const parsed = JSON.parse(stdout());
@@ -70,6 +70,45 @@ describe('runInit', () => {
70
70
  else
71
71
  process.env.AWM_HOME = originalAwmHome;
72
72
  });
73
+ // Finding #2/#3 (R7 QA): the Windows caveat used to fire via a direct
74
+ // call in registerInitCommand's Commander `.action()` closure — a call
75
+ // site no test exercised, since every other `runInit` test here bypasses
76
+ // the Commander wrapper entirely. It now lives at the top of `runInit`
77
+ // itself (the unit these tests already drive directly), firing at most
78
+ // once per run, native Windows only.
79
+ describe('Windows caveat (noteWindowsCaveat)', () => {
80
+ const realPlatform = process.platform;
81
+ let logSpy;
82
+ beforeEach(() => {
83
+ logSpy = jest.spyOn(console, 'log').mockImplementation(() => { });
84
+ });
85
+ afterEach(() => {
86
+ logSpy.mockRestore();
87
+ Object.defineProperty(process, 'platform', { value: realPlatform, configurable: true });
88
+ });
89
+ it('logs the caveat exactly once on native Windows', async () => {
90
+ Object.defineProperty(process, 'platform', { value: 'win32', configurable: true });
91
+ const { runInit } = require('../../src/commands/init');
92
+ await runInit({
93
+ cwd: tmpHome,
94
+ yes: true,
95
+ actions: { syncCache: async () => { }, installHook: () => ({ status: 'installed' }) },
96
+ });
97
+ const caveatCalls = logSpy.mock.calls.filter((c) => /awm watch/i.test(String(c[0])));
98
+ expect(caveatCalls).toHaveLength(1);
99
+ });
100
+ it('never logs the caveat on non-Windows platforms', async () => {
101
+ Object.defineProperty(process, 'platform', { value: 'linux', configurable: true });
102
+ const { runInit } = require('../../src/commands/init');
103
+ await runInit({
104
+ cwd: tmpHome,
105
+ yes: true,
106
+ actions: { syncCache: async () => { }, installHook: () => ({ status: 'installed' }) },
107
+ });
108
+ const caveatCalls = logSpy.mock.calls.filter((c) => /awm watch/i.test(String(c[0])));
109
+ expect(caveatCalls).toHaveLength(0);
110
+ });
111
+ });
73
112
  it('returns exit 1 on a bare HOME and never prompts with --yes (cache stubbed)', async () => {
74
113
  const { runInit } = require('../../src/commands/init');
75
114
  const code = await runInit({
@@ -208,6 +208,71 @@ describe('multi-agent targeting (add/remove/sync/update/doctor)', () => {
208
208
  });
209
209
  expect(outcome.code).toBe(1); // no silent catch{} — verifies Step 6's "no aborta" removal
210
210
  });
211
+ // Finding #3 (R7 QA): `noteWindowsCaveat` used to be called only from the
212
+ // raw Commander `.action()` closures in `index.ts` (update.ts:441,
213
+ // sync.ts:453 pre-fix) — call sites no test exercised, since `update`/
214
+ // `sync` are always driven here via `runUpdateCore`/`runSyncCore`
215
+ // directly. It now lives at the top of those core functions themselves,
216
+ // firing at most once per run, native Windows only.
217
+ describe('Windows caveat (noteWindowsCaveat) — update & sync', () => {
218
+ const realPlatform = process.platform;
219
+ let logSpy;
220
+ beforeEach(() => {
221
+ logSpy = jest.spyOn(console, 'log').mockImplementation(() => { });
222
+ });
223
+ afterEach(() => {
224
+ logSpy.mockRestore();
225
+ Object.defineProperty(process, 'platform', { value: realPlatform, configurable: true });
226
+ });
227
+ function caveatCalls() {
228
+ return logSpy.mock.calls.filter((c) => /awm watch/i.test(String(c[0])));
229
+ }
230
+ it('update logs the caveat exactly once on native Windows, never on Linux', async () => {
231
+ writePrefs(['claude-code']);
232
+ const { runUpdateCore } = require('../../src/commands/update');
233
+ const deps = {
234
+ syncRegistries: async () => [],
235
+ verifyMinCliVersions: () => [],
236
+ regenerateGlobalContext: () => [],
237
+ planReconciliation: () => ({ operations: [], records: [], reports: [] }),
238
+ applyInstallPlan: () => ({ installed: [], skipped: [], transactionId: 'tx', modifiedFiles: [] }),
239
+ resyncInstalledHooks: () => [],
240
+ offerSelfUpdate: async () => { },
241
+ };
242
+ Object.defineProperty(process, 'platform', { value: 'win32', configurable: true });
243
+ await runUpdateCore({}, deps);
244
+ expect(caveatCalls()).toHaveLength(1);
245
+ logSpy.mockClear();
246
+ Object.defineProperty(process, 'platform', { value: 'linux', configurable: true });
247
+ await runUpdateCore({}, deps);
248
+ expect(caveatCalls()).toHaveLength(0);
249
+ });
250
+ it('sync logs the caveat exactly once on native Windows, never on Linux', async () => {
251
+ writePrefs(['claude-code']);
252
+ const projectRoot = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-sync-project-'));
253
+ fs_1.default.mkdirSync(path_1.default.join(projectRoot, '.awm'), { recursive: true });
254
+ fs_1.default.writeFileSync(path_1.default.join(projectRoot, '.awm', 'profile.json'), JSON.stringify({ extensions: [] }));
255
+ const { runSyncCore } = require('../../src/commands/sync');
256
+ const deps = {
257
+ syncRegistries: async () => [],
258
+ verifyMinCliVersions: () => [],
259
+ verifyProjectPins: async () => [],
260
+ syncProfile: () => ({ installed: [], skipped: [], extensions: [], transactionIds: [], modifiedFiles: [] }),
261
+ };
262
+ try {
263
+ Object.defineProperty(process, 'platform', { value: 'win32', configurable: true });
264
+ await runSyncCore({ cwd: projectRoot }, deps);
265
+ expect(caveatCalls()).toHaveLength(1);
266
+ logSpy.mockClear();
267
+ Object.defineProperty(process, 'platform', { value: 'linux', configurable: true });
268
+ await runSyncCore({ cwd: projectRoot }, deps);
269
+ expect(caveatCalls()).toHaveLength(0);
270
+ }
271
+ finally {
272
+ fs_1.default.rmSync(projectRoot, { recursive: true, force: true });
273
+ }
274
+ });
275
+ });
211
276
  it('sync reports a syncProfile failure and returns a non-zero exit code instead of crashing uncaught', async () => {
212
277
  writePrefs(['claude-code'], 'claude-code');
213
278
  const projectRoot = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-sync-project-'));
@@ -237,6 +237,22 @@ describe('process identity (win32, mockeado — sin windows real disponible en e
237
237
  expect((0, process_1.refIsAlive)(fakeRef)).toBe(false);
238
238
  expect(killSpy).toHaveBeenCalledWith(999999, 0);
239
239
  });
240
+ test('pidExistsNative reintenta un ESRCH transitorio antes de declarar muerte — no confia en un unico intento (regresion: CI real windows-latest, dos corridas del mismo commit dieron resultados distintos para un proceso recien spawneado)', () => {
241
+ Object.defineProperty(process, 'platform', { value: 'win32' });
242
+ let calls = 0;
243
+ const killSpy = jest.spyOn(process, 'kill').mockImplementation(() => {
244
+ calls++;
245
+ if (calls < 3) {
246
+ const err = new Error('transient');
247
+ err.code = 'ESRCH';
248
+ throw err;
249
+ }
250
+ return true; // el pid "aparece" recien al tercer intento — simula la carrera real observada
251
+ });
252
+ const fakeRef = { pid: 424242, startTime: 'x', spawnNonce: 'n', argvDigest: 'd', processGroup: 424242, psArgsDigest: 'x' };
253
+ expect((0, process_1.refIsAlive)(fakeRef)).toBe(true); // NUNCA declara muerte por el ESRCH transitorio de los primeros 2 intentos
254
+ expect(killSpy).toHaveBeenCalledTimes(3);
255
+ });
240
256
  test('refIsAlive en win32 NUNCA declara muerte por un error que no sea ESRCH (ej. EPERM: el pid existe pero sin permiso de senializarlo) (R2.1)', () => {
241
257
  Object.defineProperty(process, 'platform', { value: 'win32' });
242
258
  jest.spyOn(process, 'kill').mockImplementation(() => {
@@ -67,17 +67,44 @@ describe('core/paths', () => {
67
67
  setPlatform('darwin');
68
68
  expect((0, paths_1.platformLabel)()).toBe('macOS');
69
69
  setPlatform('win32');
70
- expect((0, paths_1.platformLabel)()).toContain('WSL');
70
+ // Windows is a first-class, CI-verified platform since R6 — the label no
71
+ // longer hedges toward WSL, and must not silently regress back to it.
72
+ expect((0, paths_1.platformLabel)()).toBe('Windows (native, CI-verified)');
73
+ expect((0, paths_1.platformLabel)()).not.toContain('WSL');
71
74
  });
72
- it('warnIfUnsupportedPlatform calls the logger only on win32', () => {
75
+ it('noteWindowsCaveat calls the logger only on win32, with the narrow watch-supervisor gap', () => {
73
76
  const calls = [];
74
77
  const log = (m) => calls.push(m);
75
78
  setPlatform('linux');
76
- (0, paths_1.warnIfUnsupportedPlatform)(log);
79
+ (0, paths_1.noteWindowsCaveat)(log);
77
80
  expect(calls).toHaveLength(0);
78
81
  setPlatform('win32');
79
- (0, paths_1.warnIfUnsupportedPlatform)(log);
80
- expect(calls).toEqual([paths_1.WINDOWS_NATIVE_WARNING]);
82
+ (0, paths_1.noteWindowsCaveat)(log);
83
+ expect(calls).toEqual([paths_1.WINDOWS_KNOWN_GAP]);
84
+ // The message must assert Windows support, not disclaim it, and must name
85
+ // the one specific gap rather than a blanket "some things may not work"
86
+ // hedge — pinning both halves so neither regresses independently.
87
+ expect(paths_1.WINDOWS_KNOWN_GAP).toMatch(/supported and continuously verified/i);
88
+ expect(paths_1.WINDOWS_KNOWN_GAP).toMatch(/awm watch/i);
89
+ expect(paths_1.WINDOWS_KNOWN_GAP).not.toMatch(/WSL/i);
90
+ expect(paths_1.WINDOWS_KNOWN_GAP).not.toMatch(/not supported/i);
91
+ });
92
+ it('noteWindowsCaveat propagates a throwing logger instead of swallowing it', () => {
93
+ // `noteWindowsCaveat` has no try/catch around the logger call — callers
94
+ // (init.ts/update.ts/sync.ts/doctor.ts) all pass simple `console.log`
95
+ // wrappers that are not expected to throw, and every caller controls its
96
+ // own logger, so there is no shared reason for this helper to be
97
+ // defensive on their behalf. Pin that behavior explicitly: a throwing
98
+ // logger's error propagates out of `noteWindowsCaveat`, it is not
99
+ // swallowed.
100
+ setPlatform('win32');
101
+ const boom = new Error('logger exploded');
102
+ const throwingLog = () => { throw boom; };
103
+ expect(() => (0, paths_1.noteWindowsCaveat)(throwingLog)).toThrow(boom);
104
+ // And on non-Windows the logger is never even invoked, so a throwing
105
+ // logger is harmless there.
106
+ setPlatform('linux');
107
+ expect(() => (0, paths_1.noteWindowsCaveat)(throwingLog)).not.toThrow();
81
108
  });
82
109
  describe('resolveOnPath', () => {
83
110
  it('uses `command -v` on POSIX and returns true when the binary resolves', () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentic-workflow-manager",
3
- "version": "3.13.1",
3
+ "version": "3.13.3",
4
4
  "main": "dist/src/index.js",
5
5
  "bin": {
6
6
  "awm": "./dist/src/index.js"