agentic-workflow-manager 8.0.0 → 8.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.
Files changed (28) hide show
  1. package/dist/scripts/sensor-support-matrix.js +110 -12
  2. package/dist/scripts/support-matrix.js +4 -1
  3. package/dist/src/commands/preflight/index.js +4 -1
  4. package/dist/src/commands/sensors/compatibility/contract.js +142 -21
  5. package/dist/src/commands/sensors/compatibility/discovery.js +1 -1
  6. package/dist/src/commands/sensors/compatibility/live.js +40 -7
  7. package/dist/src/commands/sensors/compatibility/manifest.js +13 -3
  8. package/dist/src/commands/sensors/compatibility/materialize.js +9 -1
  9. package/dist/src/commands/sensors/compatibility/probe.js +9 -9
  10. package/dist/src/commands/sensors/compatibility/resolve.js +3 -0
  11. package/dist/src/commands/sensors/exec.js +106 -12
  12. package/dist/src/commands/sensors/init.js +6 -3
  13. package/dist/src/commands/sensors/run.js +1 -1
  14. package/dist/src/commands/sensors/status.js +1 -1
  15. package/dist/tests/commands/sensors/compatibility/contract.test.js +95 -0
  16. package/dist/tests/commands/sensors/compatibility/live.test.js +124 -0
  17. package/dist/tests/commands/sensors/compatibility/manifest.test.js +9 -0
  18. package/dist/tests/commands/sensors/compatibility/materialize.test.js +19 -0
  19. package/dist/tests/commands/sensors/compatibility/probe.test.js +10 -0
  20. package/dist/tests/commands/sensors/compatibility/python-venv-fixture.js +29 -0
  21. package/dist/tests/commands/sensors/compatibility/resolve.test.js +8 -0
  22. package/dist/tests/commands/sensors/exec-windows.test.js +58 -10
  23. package/dist/tests/commands/sensors/exec.test.js +51 -3
  24. package/dist/tests/commands/sensors/init.test.js +72 -0
  25. package/dist/tests/integration/preflight-json-pipe.e2e.test.js +26 -0
  26. package/dist/tests/structural/r3-cli-major-version.test.js +9 -7
  27. package/dist/tests/structural/support-matrix-is-current.test.js +78 -6
  28. package/package.json +3 -2
@@ -12,6 +12,14 @@ const sensor = { applicability: { allFiles: ['package.json'] }, variants: [varia
12
12
  const evidence = (input = {}) => ({ paths: ['package.json'], applicable: true, packageManagerConflict: false, toolVersion: '10.4.1', runtimeVersion: '22.0.0', probe: { status: 'matched' }, ...input });
13
13
  const context = { pack: 'js-ts', sensor: 'lint' };
14
14
  describe('resolveSensorCompatibility', () => {
15
+ it('keeps an automatically selected generic pack not-applicable despite language markers', () => {
16
+ const resolved = (0, resolve_1.resolveSensorCompatibility)({ applicability: { kind: 'explicit-or-supported-language' }, variants: [variant('semgrep')] }, evidence({ applicable: undefined, paths: ['pyproject.toml'], toolVersion: '1.0.0', runtimeVersion: '3.12.0', probe: { status: 'matched' } }), { pack: 'generic', sensor: 'security' });
17
+ expect(resolved).toMatchObject({ state: 'not-applicable', reason: 'applicability-not-met' });
18
+ });
19
+ it('treats the persisted explicit generic pack selection as positive capability in project resolution', () => {
20
+ const resolved = (0, resolve_1.resolveProjectCompatibility)({ schemaVersion: 2, name: 'generic', sensors: { security: { applicability: { kind: 'explicit-or-supported-language' }, variants: [variant('semgrep')] } } }, evidence({ applicable: undefined, paths: [], packSelection: 'explicit' }));
21
+ expect(resolved.sensors.security.state).not.toBe('not-applicable');
22
+ });
15
23
  test.each([
16
24
  ['certified', evidence(), 'eslint-main'],
17
25
  ['compatible-unverified', evidence({ toolVersion: '11.0.0' }), 'eslint-main'],
@@ -24,6 +24,12 @@ function fakeChild(pid = 4242) {
24
24
  child.unref = jest.fn();
25
25
  return child;
26
26
  }
27
+ function restoreEnvironment(name, value) {
28
+ if (value === undefined)
29
+ delete process.env[name];
30
+ else
31
+ process.env[name] = value;
32
+ }
27
33
  describe('runCommand — win32', () => {
28
34
  const originalPlatform = process.platform;
29
35
  beforeEach(() => {
@@ -58,11 +64,35 @@ describe('runCommand — win32', () => {
58
64
  expect(mockSpawn).toHaveBeenCalledWith(path_1.default.join(dir, 'tool.exe'), ['literal;&'], expect.objectContaining({ shell: false, detached: false }));
59
65
  child.emit('close', 0, null);
60
66
  await expect(pending).resolves.toMatchObject({ code: 0 });
61
- expect(() => (0, exec_1.runStructuredCommand)({ executable: 'tool.cmd', resolution: 'path', args: [] }, { timeout: 5000, cwd: dir })).toThrow(/wrappers/);
67
+ expect(() => (0, exec_1.runStructuredCommand)({ executable: 'tool.cmd', resolution: 'path', args: ['--version'] }, { timeout: 5000, cwd: dir })).toThrow(/wrappers/);
68
+ }
69
+ finally {
70
+ restoreEnvironment('PATH', savedPath);
71
+ restoreEnvironment('PATHEXT', savedPathExt);
72
+ fs_1.default.rmSync(dir, { recursive: true, force: true });
73
+ }
74
+ });
75
+ it('spawns the allowlisted flat-config environment for ESLint 8', async () => {
76
+ const dir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-structured-win-env-'));
77
+ const savedPath = process.env.PATH;
78
+ const savedPathExt = process.env.PATHEXT;
79
+ try {
80
+ fs_1.default.writeFileSync(path_1.default.join(dir, 'tool.exe'), 'fixture');
81
+ process.env.PATH = dir;
82
+ process.env.PATHEXT = '.EXE';
83
+ const child = fakeChild();
84
+ mockSpawn.mockReturnValue(child);
85
+ const pending = (0, exec_1.runStructuredCommand)({ executable: 'tool', resolution: 'path', args: ['--version'], environment: { ESLINT_USE_FLAT_CONFIG: 'true' } }, { timeout: 5000, cwd: dir });
86
+ expect(mockSpawn).toHaveBeenCalledWith(path_1.default.join(dir, 'tool.exe'), ['--version'], expect.objectContaining({
87
+ shell: false,
88
+ env: expect.objectContaining({ ESLINT_USE_FLAT_CONFIG: 'true' }),
89
+ }));
90
+ child.emit('close', 0, null);
91
+ await expect(pending).resolves.toMatchObject({ code: 0 });
62
92
  }
63
93
  finally {
64
- process.env.PATH = savedPath;
65
- process.env.PATHEXT = savedPathExt;
94
+ restoreEnvironment('PATH', savedPath);
95
+ restoreEnvironment('PATHEXT', savedPathExt);
66
96
  fs_1.default.rmSync(dir, { recursive: true, force: true });
67
97
  }
68
98
  });
@@ -80,14 +110,14 @@ describe('runCommand — win32', () => {
80
110
  process.env.PATHEXT = '.EXE';
81
111
  const child = fakeChild();
82
112
  mockSpawn.mockReturnValue(child);
83
- const pending = (0, exec_1.runStructuredCommand)({ executable: 'tool', resolution: 'node-modules-bin', args: [] }, { timeout: 5000, cwd: dir });
84
- expect(mockSpawn).toHaveBeenCalledWith(path_1.default.join(localBin, 'tool.exe'), [], expect.objectContaining({ shell: false }));
113
+ const pending = (0, exec_1.runStructuredCommand)({ executable: 'tool', resolution: 'node-modules-bin', args: ['--version'] }, { timeout: 5000, cwd: dir });
114
+ expect(mockSpawn).toHaveBeenCalledWith(path_1.default.join(localBin, 'tool.exe'), ['--version'], expect.objectContaining({ shell: false }));
85
115
  child.emit('close', 0, null);
86
116
  await expect(pending).resolves.toMatchObject({ code: 0 });
87
117
  }
88
118
  finally {
89
- process.env.PATH = savedPath;
90
- process.env.PATHEXT = savedPathExt;
119
+ restoreEnvironment('PATH', savedPath);
120
+ restoreEnvironment('PATHEXT', savedPathExt);
91
121
  fs_1.default.rmSync(dir, { recursive: true, force: true });
92
122
  fs_1.default.rmSync(global, { recursive: true, force: true });
93
123
  }
@@ -100,16 +130,34 @@ describe('runCommand — win32', () => {
100
130
  fs_1.default.writeFileSync(path_1.default.join(global, 'tool.exe'), 'global');
101
131
  process.env.PATH = global;
102
132
  mockSpawn.mockReturnValue(fakeChild());
103
- expect(() => (0, exec_1.runStructuredCommand)({ executable: 'tool', resolution: 'node-modules-bin', args: [] }, { timeout: 5000, cwd: dir }))
133
+ expect(() => (0, exec_1.runStructuredCommand)({ executable: 'tool', resolution: 'node-modules-bin', args: ['--version'] }, { timeout: 5000, cwd: dir }))
104
134
  .toThrow(/node_modules.*not found locally/i);
105
135
  expect(mockSpawn).not.toHaveBeenCalled();
106
136
  }
107
137
  finally {
108
- process.env.PATH = savedPath;
138
+ restoreEnvironment('PATH', savedPath);
109
139
  fs_1.default.rmSync(dir, { recursive: true, force: true });
110
140
  fs_1.default.rmSync(global, { recursive: true, force: true });
111
141
  }
112
142
  });
143
+ it('fails closed when PATHEXT contains no safe executable extension', () => {
144
+ const dir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-structured-win-pathext-'));
145
+ const savedPath = process.env.PATH;
146
+ const savedPathExt = process.env.PATHEXT;
147
+ try {
148
+ fs_1.default.writeFileSync(path_1.default.join(dir, 'tool.cmd'), 'wrapper');
149
+ process.env.PATH = dir;
150
+ process.env.PATHEXT = '.CMD';
151
+ expect(() => (0, exec_1.runStructuredCommand)({ executable: 'tool', resolution: 'path', args: ['--version'] }, { timeout: 5000, cwd: dir }))
152
+ .toThrow(/safe Windows executable|PATHEXT/i);
153
+ expect(mockSpawn).not.toHaveBeenCalled();
154
+ }
155
+ finally {
156
+ restoreEnvironment('PATH', savedPath);
157
+ restoreEnvironment('PATHEXT', savedPathExt);
158
+ fs_1.default.rmSync(dir, { recursive: true, force: true });
159
+ }
160
+ });
113
161
  it('rejects a missing Python environment executable instead of falling back to a same-named global command', () => {
114
162
  const dir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-python-env-missing-win-'));
115
163
  const global = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-python-env-global-win-'));
@@ -123,7 +171,7 @@ describe('runCommand — win32', () => {
123
171
  expect(mockSpawn).not.toHaveBeenCalled();
124
172
  }
125
173
  finally {
126
- process.env.PATH = savedPath;
174
+ restoreEnvironment('PATH', savedPath);
127
175
  fs_1.default.rmSync(dir, { recursive: true, force: true });
128
176
  fs_1.default.rmSync(global, { recursive: true, force: true });
129
177
  }
@@ -27,7 +27,7 @@ describe('runCommand — exit codes and output', () => {
27
27
  const literal = `;touch ${marker}`;
28
28
  const received = path_1.default.join(dir, 'received');
29
29
  const result = await (0, exec_1.runStructuredCommand)({
30
- executable: process.execPath,
30
+ executable: 'node',
31
31
  resolution: 'path',
32
32
  args: ['-e', "require('fs').writeFileSync(process.argv[1], process.argv[2])", received, literal],
33
33
  }, { timeout: 5000, cwd: dir });
@@ -130,6 +130,36 @@ describe('runCommand — spawn failure', () => {
130
130
  expect(r.code).not.toBe(0);
131
131
  });
132
132
  });
133
+ describe('runStructuredCommand — public boundary validation', () => {
134
+ it.each(['sh', 'cmd.exe', '/usr/bin/node', 'nested/tool', 'nested\\tool'])('rejects unsafe executable %j before resolution', executable => {
135
+ expect(() => (0, exec_1.runStructuredCommand)({ executable, resolution: 'path', args: ['--version'] }, { timeout: 5000, cwd: process.cwd() }))
136
+ .toThrow(/safe executable name|shell/i);
137
+ });
138
+ it('normalizes package-manager spellings while requiring an equivalent explicit selection', () => {
139
+ expect(() => (0, exec_1.runStructuredCommand)({ executable: 'NPM.exe', resolution: 'path', args: ['--version'] }, { timeout: 5000, cwd: process.cwd() }))
140
+ .toThrow(/packageManager/);
141
+ expect(() => (0, exec_1.runStructuredCommand)({ executable: 'NPM.exe', resolution: 'path', args: ['--version'], packageManager: 'pnpm' }, { timeout: 5000, cwd: process.cwd() }))
142
+ .toThrow(/packageManager/);
143
+ });
144
+ it.each([
145
+ { ESLINT_USE_FLAT_CONFIG: 'invalid' },
146
+ { ESLINT_USE_FLAT_CONFIG: 'true', OTHER: 'value' },
147
+ ])('rejects an unknown or expanded environment mapping at the public boundary', environment => {
148
+ expect(() => (0, exec_1.runStructuredCommand)({ executable: 'node', resolution: 'path', args: ['--version'], environment }, { timeout: 5000, cwd: process.cwd() }))
149
+ .toThrow(/exact allowlisted/);
150
+ });
151
+ it.each([
152
+ [{ executable: 'node', resolution: 'path', args: [] }, /nonempty array/],
153
+ [{ executable: 'node', resolution: 'path', args: [''] }, /nonempty single-line/],
154
+ [{ executable: 'node', resolution: 'path', args: ['one\ntwo'] }, /single-line/],
155
+ [{ executable: 'node', resolution: 'path', args: ['{files}'] }, /requires fileInput/],
156
+ [{ executable: 'node', resolution: 'path', args: ['prefix{files}'], fileInput: { placeholder: '{files}', extensions: ['.ts'] } }, /must not embed/],
157
+ [{ executable: 'node', resolution: 'path', args: ['{files}', '{files}'], fileInput: { placeholder: '{files}', extensions: ['.ts'] } }, /exactly one/],
158
+ [{ executable: 'node', resolution: 'path', args: ['--check'], fileInput: { placeholder: '{files}', extensions: ['.ts'] } }, /exactly one/],
159
+ ])('rejects malformed args or file input at the public boundary', (command, message) => {
160
+ expect(() => (0, exec_1.runStructuredCommand)(command, { timeout: 5000, cwd: process.cwd() })).toThrow(message);
161
+ });
162
+ });
133
163
  onPosix('runStructuredCommand — local node_modules binaries', () => {
134
164
  it('follows an npm-style contained shim but rejects one escaping node_modules', async () => {
135
165
  const dir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-local-bin-symlink-'));
@@ -143,11 +173,11 @@ onPosix('runStructuredCommand — local node_modules binaries', () => {
143
173
  fs_1.default.writeFileSync(outside, '#!/bin/sh\necho outside\n', { mode: 0o755 });
144
174
  fs_1.default.mkdirSync(bin, { recursive: true });
145
175
  fs_1.default.symlinkSync('../tool/bin/tool', path_1.default.join(bin, 'tool'));
146
- await expect((0, exec_1.runStructuredCommand)({ executable: 'tool', resolution: 'node-modules-bin', args: [] }, { timeout: 5000, cwd: dir }))
176
+ await expect((0, exec_1.runStructuredCommand)({ executable: 'tool', resolution: 'node-modules-bin', args: ['--version'] }, { timeout: 5000, cwd: dir }))
147
177
  .resolves.toMatchObject({ code: 0, stdout: 'contained\n' });
148
178
  fs_1.default.unlinkSync(path_1.default.join(bin, 'tool'));
149
179
  fs_1.default.symlinkSync(outside, path_1.default.join(bin, 'tool'));
150
- expect(() => (0, exec_1.runStructuredCommand)({ executable: 'tool', resolution: 'node-modules-bin', args: [] }, { timeout: 5000, cwd: dir }))
180
+ expect(() => (0, exec_1.runStructuredCommand)({ executable: 'tool', resolution: 'node-modules-bin', args: ['--version'] }, { timeout: 5000, cwd: dir }))
151
181
  .toThrow(/node_modules.*contain|local/i);
152
182
  }
153
183
  finally {
@@ -156,6 +186,24 @@ onPosix('runStructuredCommand — local node_modules binaries', () => {
156
186
  });
157
187
  });
158
188
  onPosix('runStructuredCommand — Python environments', () => {
189
+ it.each(['.venv', 'venv'])('rejects a symlinked %s ancestor instead of executing outside the project', environment => {
190
+ const dir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-python-env-ancestor-'));
191
+ const outside = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-python-env-outside-'));
192
+ const marker = path_1.default.join(dir, 'must-not-exist');
193
+ try {
194
+ const executable = path_1.default.join(outside, 'bin', 'semgrep');
195
+ fs_1.default.mkdirSync(path_1.default.dirname(executable), { recursive: true });
196
+ fs_1.default.writeFileSync(executable, `#!/bin/sh\ntouch ${marker}\n`, { mode: 0o755 });
197
+ fs_1.default.symlinkSync(outside, path_1.default.join(dir, environment));
198
+ expect(() => (0, exec_1.runStructuredCommand)({ executable: 'semgrep', resolution: 'python-environment', pythonEnvironmentRoot: environment, args: ['--validate'] }, { timeout: 5000, cwd: dir }))
199
+ .toThrow(/python.*environment.*local|contained/i);
200
+ expect(fs_1.default.existsSync(marker)).toBe(false);
201
+ }
202
+ finally {
203
+ fs_1.default.rmSync(dir, { recursive: true, force: true });
204
+ fs_1.default.rmSync(outside, { recursive: true, force: true });
205
+ }
206
+ });
159
207
  it('rejects a missing project Python executable instead of falling back to PATH', () => {
160
208
  const dir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-python-env-missing-'));
161
209
  const global = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-python-env-global-'));
@@ -7,6 +7,7 @@ const fs_1 = __importDefault(require("fs"));
7
7
  const path_1 = __importDefault(require("path"));
8
8
  const os_1 = __importDefault(require("os"));
9
9
  const init_1 = require("../../../src/commands/sensors/init");
10
+ const status_1 = require("../../../src/commands/sensors/status");
10
11
  // Build a throwaway registry with a js-ts pack.json (the single source of truth
11
12
  // init now reads from). `defaultCmd` uses the {{SOURCE_DIRS}} placeholder for depcheck.
12
13
  function makeRegistry() {
@@ -45,9 +46,11 @@ function makeV2Registry() {
45
46
  const packDir = path_1.default.join(registryRoot, 'sensor-packs', 'js-ts');
46
47
  fs_1.default.mkdirSync(packDir, { recursive: true });
47
48
  fs_1.default.writeFileSync(path_1.default.join(packDir, 'eslint.config.awm.mjs'), 'export default []\n');
49
+ fs_1.default.writeFileSync(path_1.default.join(packDir, 'tsconfig.awm.json'), '{ "compilerOptions": { "strict": true } }\n');
48
50
  fs_1.default.writeFileSync(path_1.default.join(packDir, 'pack.json'), JSON.stringify({
49
51
  schemaVersion: 2, name: 'js-ts', description: 'fixture', detects: ['package.json'],
50
52
  coverage: { schemaVersion: 1, classes: { lint: { description: 'lint', detectors: [{ sensor: 'lint' }], remedy: { summary: 'fix lint', command: 'awm sensors init --pack js-ts' } } } },
53
+ hardening: { 'typescript-strict': { assets: ['tsconfig.awm.json'] } },
51
54
  sensors: { lint: { applicability: { allFiles: ['package.json'] }, variants: [{
52
55
  id: 'eslint-10', priority: 10, certifiedRange: '>=10.0.0 <11.0.0',
53
56
  requirements: { tool: 'eslint', toolRange: '>=10.0.0 <11.0.0', runtime: 'node', runtimeRange: '>=0.0.0' },
@@ -57,6 +60,23 @@ function makeV2Registry() {
57
60
  }));
58
61
  return registryRoot;
59
62
  }
63
+ function makeGenericV2Registry() {
64
+ const registryRoot = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-reg-generic-v2-'));
65
+ const packDir = path_1.default.join(registryRoot, 'sensor-packs', 'generic');
66
+ fs_1.default.mkdirSync(packDir, { recursive: true });
67
+ fs_1.default.writeFileSync(path_1.default.join(packDir, 'generic.config'), 'fixture\n');
68
+ fs_1.default.writeFileSync(path_1.default.join(packDir, 'pack.json'), JSON.stringify({
69
+ schemaVersion: 2, name: 'generic', description: 'fixture', detects: ['README.md'],
70
+ coverage: { schemaVersion: 1, classes: { security: { description: 'security', detectors: [{ sensor: 'security' }], remedy: { summary: 'run security', command: 'awm sensors init --pack generic' } } } },
71
+ sensors: { security: { applicability: { kind: 'explicit-or-supported-language' }, variants: [{
72
+ id: 'eslint-10', priority: 10, certifiedRange: '>=10.0.0 <11.0.0',
73
+ requirements: { tool: 'eslint', toolRange: '>=10.0.0 <11.0.0', runtime: 'node', runtimeRange: '>=0.0.0', configFiles: ['generic.config'] },
74
+ assets: ['generic.config'], formatter: 'eslint-llm', probe: { kind: 'config-present' },
75
+ command: { executable: 'eslint', resolution: 'node-modules-bin', args: ['.'] },
76
+ }] } },
77
+ }));
78
+ return registryRoot;
79
+ }
60
80
  describe('detectStack', () => {
61
81
  let tmpDir;
62
82
  beforeEach(() => { tmpDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-init-')); });
@@ -277,6 +297,58 @@ describe('initSensors', () => {
277
297
  fs_1.default.rmSync(v2Registry, { recursive: true, force: true });
278
298
  }
279
299
  });
300
+ it('persists an explicit generic selection for live revalidation without promoting automatic generic fallback', async () => {
301
+ const v2Registry = makeGenericV2Registry();
302
+ try {
303
+ fs_1.default.mkdirSync(path_1.default.join(tmpDir, 'node_modules', 'eslint'), { recursive: true });
304
+ fs_1.default.writeFileSync(path_1.default.join(tmpDir, 'node_modules', 'eslint', 'package.json'), JSON.stringify({ version: '10.0.0' }));
305
+ const automatic = await (0, init_1.initSensors)({ cwd: tmpDir, registryRoot: v2Registry, configure: false });
306
+ expect(automatic.manifest).toMatchObject({ schemaVersion: 2, pack: 'generic', sensors: {} });
307
+ expect(automatic.manifest.packSelection).toBeUndefined();
308
+ const explicit = await (0, init_1.initSensors)({ cwd: tmpDir, registryRoot: v2Registry, pack: 'generic' });
309
+ const written = JSON.parse(fs_1.default.readFileSync(path_1.default.join(tmpDir, '.awm', 'sensors.json'), 'utf8'));
310
+ expect(explicit.manifest).toMatchObject({ schemaVersion: 2, pack: 'generic', packSelection: 'explicit', sensors: { security: { variantId: 'eslint-10' } } });
311
+ expect(written.packSelection).toBe('explicit');
312
+ await expect((0, status_1.computeSensorStatus)(tmpDir)).resolves.toMatchObject({ overall: 'HEALTHY', checks: { security: { ok: true } } });
313
+ }
314
+ finally {
315
+ fs_1.default.rmSync(v2Registry, { recursive: true, force: true });
316
+ }
317
+ });
318
+ it('never materializes an opt-in hardening asset during ordinary v2 init', async () => {
319
+ const v2Registry = makeV2Registry();
320
+ try {
321
+ fs_1.default.writeFileSync(path_1.default.join(tmpDir, 'package.json'), JSON.stringify({ devDependencies: { eslint: '^10.0.0' } }));
322
+ fs_1.default.mkdirSync(path_1.default.join(tmpDir, 'node_modules', 'eslint'), { recursive: true });
323
+ fs_1.default.writeFileSync(path_1.default.join(tmpDir, 'node_modules', 'eslint', 'package.json'), JSON.stringify({ version: '10.0.0' }));
324
+ await (0, init_1.initSensors)({ cwd: tmpDir, registryRoot: v2Registry });
325
+ expect(fs_1.default.existsSync(path_1.default.join(tmpDir, 'eslint.config.awm.mjs'))).toBe(true);
326
+ expect(fs_1.default.existsSync(path_1.default.join(tmpDir, 'tsconfig.awm.json'))).toBe(false);
327
+ }
328
+ finally {
329
+ fs_1.default.rmSync(v2Registry, { recursive: true, force: true });
330
+ }
331
+ });
332
+ it('initializes a selected v2 variant with an explicitly empty assets list', async () => {
333
+ const v2Registry = makeV2Registry();
334
+ try {
335
+ const packPath = path_1.default.join(v2Registry, 'sensor-packs', 'js-ts', 'pack.json');
336
+ const pack = JSON.parse(fs_1.default.readFileSync(packPath, 'utf8'));
337
+ pack.sensors.lint.variants[0].assets = [];
338
+ fs_1.default.writeFileSync(packPath, JSON.stringify(pack));
339
+ fs_1.default.writeFileSync(path_1.default.join(tmpDir, 'package.json'), JSON.stringify({ devDependencies: { eslint: '^10.0.0' } }));
340
+ fs_1.default.mkdirSync(path_1.default.join(tmpDir, 'node_modules', 'eslint'), { recursive: true });
341
+ fs_1.default.writeFileSync(path_1.default.join(tmpDir, 'node_modules', 'eslint', 'package.json'), JSON.stringify({ version: '10.0.0' }));
342
+ await expect((0, init_1.initSensors)({ cwd: tmpDir, registryRoot: v2Registry })).resolves.toMatchObject({
343
+ manifest: { schemaVersion: 2, sensors: { lint: { assets: [] } } },
344
+ configured: [],
345
+ });
346
+ expect(fs_1.default.existsSync(path_1.default.join(tmpDir, 'eslint.config.awm.mjs'))).toBe(false);
347
+ }
348
+ finally {
349
+ fs_1.default.rmSync(v2Registry, { recursive: true, force: true });
350
+ }
351
+ });
280
352
  it('preserves enabled and fast choices when a valid v2 manifest is re-initialized', async () => {
281
353
  const v2Registry = makeV2Registry();
282
354
  try {
@@ -0,0 +1,26 @@
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
+ const child_process_1 = require("child_process");
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const os_1 = __importDefault(require("os"));
9
+ const path_1 = __importDefault(require("path"));
10
+ const cliRoot = path_1.default.resolve(__dirname, '../..');
11
+ const bin = path_1.default.join(cliRoot, 'dist', 'src', 'index.js');
12
+ test('preserves degraded preflight JSON when stdout is piped', () => {
13
+ const project = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-preflight-pipe-'));
14
+ try {
15
+ const result = (0, child_process_1.spawnSync)(process.execPath, [bin, 'preflight', '--json'], {
16
+ cwd: project,
17
+ encoding: 'utf8',
18
+ env: { ...process.env, AWM_HOME: path_1.default.join(project, 'awm-home'), AWM_NO_UPDATE_CHECK: '1' },
19
+ });
20
+ expect(result.status).toBe(1);
21
+ expect(JSON.parse(result.stdout)).toMatchObject({ status: 'not_configured' });
22
+ }
23
+ finally {
24
+ fs_1.default.rmSync(project, { recursive: true, force: true });
25
+ }
26
+ });
@@ -8,7 +8,7 @@ const path_1 = __importDefault(require("path"));
8
8
  const child_process_1 = require("child_process");
9
9
  const CLI_ROOT = path_1.default.resolve(__dirname, '../..');
10
10
  const DIST_ENTRYPOINT = path_1.default.join(CLI_ROOT, 'dist', 'src', 'index.js');
11
- const TARGET_VERSION = '7.0.0';
11
+ const R3_MAJOR_VERSION = 8;
12
12
  function readJson(file) {
13
13
  return JSON.parse(fs_1.default.readFileSync(path_1.default.join(CLI_ROOT, file), 'utf8'));
14
14
  }
@@ -22,16 +22,18 @@ function runCompiledCli(...args) {
22
22
  return `${result.stdout}${result.stderr}`;
23
23
  }
24
24
  describe('R3 public major CLI release contract', () => {
25
- it('declares 7.0.0 consistently in package metadata and the lockfile root', () => {
25
+ it('retains the R3 major version consistently in package metadata and the lockfile root', () => {
26
26
  const pkg = readJson('package.json');
27
27
  const lock = readJson('package-lock.json');
28
28
  const root = lock.packages[''];
29
- expect(pkg.version).toBe(TARGET_VERSION);
30
- expect(lock.version).toBe(TARGET_VERSION);
31
- expect(root.version).toBe(TARGET_VERSION);
29
+ const version = pkg.version;
30
+ expect(typeof version).toBe('string');
31
+ expect(version).toMatch(new RegExp(`^${R3_MAJOR_VERSION}\\.`));
32
+ expect(lock.version).toBe(version);
33
+ expect(root.version).toBe(version);
32
34
  });
33
- it('exposes 7.0.0 from the compiled CLI while retaining its help banner', () => {
34
- expect(runCompiledCli('--version').trim()).toBe(TARGET_VERSION);
35
+ it('exposes the package version from the compiled CLI while retaining its help banner', () => {
36
+ expect(runCompiledCli('--version').trim()).toBe(readJson('package.json').version);
35
37
  expect(runCompiledCli('--help')).toContain('Usage: awm');
36
38
  });
37
39
  });
@@ -16,10 +16,13 @@ Object.defineProperty(exports, "__esModule", { value: true });
16
16
  // Este test hace que ese desvio sea imposible de mergear. No verifica que la tabla sea
17
17
  // "linda": verifica que sea LA MISMA que produce el codigo hoy.
18
18
  const fs_1 = __importDefault(require("fs"));
19
+ const os_1 = __importDefault(require("os"));
19
20
  const path_1 = __importDefault(require("path"));
21
+ const child_process_1 = require("child_process");
20
22
  const support_matrix_1 = require("../../scripts/support-matrix");
21
23
  const sensor_support_matrix_1 = require("../../scripts/sensor-support-matrix");
22
24
  const SENSOR_FIXTURE_REGISTRY = path_1.default.join(__dirname, '..', 'fixtures', 'sensor-support-matrix', 'registry');
25
+ const CI_WORKFLOW_PATH = path_1.default.resolve(__dirname, '../../..', '.github', 'workflows', 'ci.yml');
23
26
  describe('docs/support-matrix.md refleja el codigo', () => {
24
27
  it('el bloque generado esta al dia', () => {
25
28
  const doc = fs_1.default.readFileSync(support_matrix_1.DOC_PATH, 'utf-8');
@@ -33,17 +36,76 @@ describe('docs/support-matrix.md refleja el codigo', () => {
33
36
  expect(doc).toContain(support_matrix_1.BEGIN_MARKER);
34
37
  expect(doc).toContain(support_matrix_1.END_MARKER);
35
38
  });
36
- it('the generated pre-publication v2 sensor-pack contract fixture is current', () => {
39
+ it('keeps the generated pre-publication v2 fixture separately renderable', () => {
37
40
  const doc = fs_1.default.readFileSync(support_matrix_1.DOC_PATH, 'utf-8');
41
+ const fixture = (0, sensor_support_matrix_1.renderSensorSupportMatrix)(SENSOR_FIXTURE_REGISTRY);
42
+ expect(fixture).toContain(sensor_support_matrix_1.R3_PREPUBLICATION_FIXTURE_PURPOSE);
43
+ expect(fixture).toContain('not the published `awm-baseline-registry` manifests');
44
+ expect(fixture).not.toContain('Published certification evidence');
38
45
  expect(doc).toContain(sensor_support_matrix_1.SENSOR_BEGIN_MARKER);
39
46
  expect(doc).toContain(sensor_support_matrix_1.SENSOR_END_MARKER);
40
- expect(doc).toContain(sensor_support_matrix_1.R3_PREPUBLICATION_FIXTURE_PURPOSE);
41
- expect(doc).toContain('not the published `awm-baseline-registry` manifests');
42
- expect(doc).toBe((0, sensor_support_matrix_1.spliceSensorSupportMatrix)(doc, (0, sensor_support_matrix_1.renderSensorSupportMatrix)(SENSOR_FIXTURE_REGISTRY)));
47
+ expect(doc).toContain('published registry evidence');
48
+ expect(doc).toContain('Published certification evidence');
43
49
  });
44
- it('pins the documentation generator to the declared R3 pre-publication fixture', () => {
50
+ it('uses a published registry root for documentation and retains a fixture-only command', () => {
45
51
  const packageJson = JSON.parse(fs_1.default.readFileSync(path_1.default.join(__dirname, '..', '..', 'package.json'), 'utf8'));
46
- expect(packageJson.scripts?.['docs:matrix']).toContain(`--registry-root ${sensor_support_matrix_1.R3_PREPUBLICATION_FIXTURE_RELATIVE_PATH}`);
52
+ expect(packageJson.scripts?.['docs:matrix']).toContain('--registry-root ../../awm-baseline-registry');
53
+ expect(packageJson.scripts?.['docs:matrix:prepublication']).toContain(`--registry-root ${sensor_support_matrix_1.R3_PREPUBLICATION_FIXTURE_RELATIVE_PATH}`);
54
+ });
55
+ it('CI regenerates the published matrix from the immutable registry tag', () => {
56
+ const workflow = fs_1.default.readFileSync(CI_WORKFLOW_PATH, 'utf8');
57
+ expect(workflow).toContain('repository: Kodria/awm-baseline-registry');
58
+ expect(workflow).toContain('ref: v2.0.0');
59
+ expect(workflow).toContain('path: awm-baseline-registry');
60
+ expect(workflow).toContain('Verify published sensor support matrix');
61
+ expect(workflow).toContain('--registry-root ../awm-baseline-registry --registry-tag v2.0.0 --registry-commit c35c087a0801c0b4e69e0a4ac3eafef9ecdf37cd');
62
+ expect(workflow).toContain('git diff --exit-code -- docs/support-matrix.md');
63
+ });
64
+ it('retains the published registry certification rows verbatim instead of inventing fixture evidence', () => {
65
+ const publishedSupport = [
66
+ '# Sensor pack support',
67
+ '',
68
+ '<!-- BEGIN GENERATED: sensor-pack-support -->',
69
+ 'Generated from pack manifests and certification pins resolved at `2026-08-14T22:14:34.365Z`.',
70
+ '',
71
+ 'Status: `certified` has a matching frozen tool pin; `compatible-unverified` has no matching frozen pin; `not-applicable` is reserved for variants without a tool contract.',
72
+ '',
73
+ '| Pack | Sensor | Variant | Tool | Certified range | Supported OS | OS certification evidence | Status | Evidence |',
74
+ '| --- | --- | --- | --- | --- | --- | --- | --- | --- |',
75
+ '| `js-ts` | `lint` | `eslint-10-flat` | `eslint` | `=10.8.1` | Ubuntu, macOS, Windows | Ubuntu/macOS/Windows: contract | certified | pin: `eslint@10.8.1` |',
76
+ '| `js-ts` | `test` | `npm-script` | `npm` | `>=8.0.0` | Ubuntu, macOS, Windows | Ubuntu/macOS/Windows: contract | compatible-unverified | no matching pinned tool |',
77
+ '',
78
+ '| Certification status | Derived variant count | Meaning |',
79
+ '| --- | --- | --- |',
80
+ '| certified | 15 | Matching frozen tool pin |',
81
+ '| compatible-unverified | 6 | No matching frozen tool pin |',
82
+ '| not-applicable | 0 | Variant has no tool contract |',
83
+ '<!-- END GENERATED: sensor-pack-support -->',
84
+ '',
85
+ ].join('\n');
86
+ expect((0, sensor_support_matrix_1.extractPublishedSupportMetadata)(publishedSupport)).toContain('`eslint-10-flat`');
87
+ expect((0, sensor_support_matrix_1.extractPublishedSupportMetadata)(publishedSupport)).toContain('compatible-unverified');
88
+ expect((0, sensor_support_matrix_1.extractPublishedSupportMetadata)(publishedSupport)).not.toContain('Fixture-declared ranges only');
89
+ });
90
+ it('rejects a mutable checkout even when it contains the published v2.0.0 tag', () => {
91
+ const registryRoot = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-published-registry-'));
92
+ const git = (...args) => (0, child_process_1.execFileSync)('git', ['-C', registryRoot, ...args], { stdio: 'pipe' });
93
+ try {
94
+ git('init');
95
+ git('config', 'user.email', 'test@example.com');
96
+ git('config', 'user.name', 'AWM test');
97
+ fs_1.default.writeFileSync(path_1.default.join(registryRoot, 'published.txt'), 'published\n');
98
+ git('add', '.');
99
+ git('commit', '-m', 'published registry');
100
+ git('tag', 'v2.0.0');
101
+ fs_1.default.writeFileSync(path_1.default.join(registryRoot, 'mutable.txt'), 'newer checkout\n');
102
+ git('add', '.');
103
+ git('commit', '-m', 'mutable checkout');
104
+ expect(() => (0, sensor_support_matrix_1.verifyPublishedRegistryIdentity)(registryRoot, 'v2.0.0', 'c35c087a0801c0b4e69e0a4ac3eafef9ecdf37cd')).toThrow(/HEAD .*expected commit/i);
105
+ }
106
+ finally {
107
+ fs_1.default.rmSync(registryRoot, { recursive: true, force: true });
108
+ }
47
109
  });
48
110
  it('la tabla nombra a los seis providers declarados', () => {
49
111
  // Que el bloque este "al dia" no sirve si el generador se olvidara de un provider:
@@ -156,6 +218,16 @@ describe('docs/support-matrix.md refleja el codigo', () => {
156
218
  expect(out).toContain('linea uno\r\nlinea dos');
157
219
  expect(out.split('\r\n').length - 1).toBe(out.split('\n').length - 1); // ni un LF suelto
158
220
  });
221
+ it('preserva el EOL del bloque de providers aunque otro bloque sea CRLF', () => {
222
+ const mixedDoc = `intro\r\n${support_matrix_1.BEGIN_MARKER}\nold\n${support_matrix_1.END_MARKER}\nfin\n`;
223
+ const out = (0, support_matrix_1.spliceGenerated)(mixedDoc, 'linea uno\nlinea dos');
224
+ expect(out).toContain(`${support_matrix_1.BEGIN_MARKER}\n\nlinea uno\nlinea dos\n\n${support_matrix_1.END_MARKER}`);
225
+ });
226
+ it('preserva el EOL del bloque de sensores aunque otro bloque sea CRLF', () => {
227
+ const mixedDoc = `intro\r\n${sensor_support_matrix_1.SENSOR_BEGIN_MARKER}\nold\n${sensor_support_matrix_1.SENSOR_END_MARKER}\nfin\n`;
228
+ const out = (0, sensor_support_matrix_1.spliceSensorSupportMatrix)(mixedDoc, 'linea uno\nlinea dos');
229
+ expect(out).toContain(`${sensor_support_matrix_1.SENSOR_BEGIN_MARKER}\n\nlinea uno\nlinea dos\n\n${sensor_support_matrix_1.SENSOR_END_MARKER}`);
230
+ });
159
231
  it('marks an unsupported scope rather than leaving it absent', () => {
160
232
  // La diferencia entre "no soportado" y una celda vacia es exactamente lo que el
161
233
  // documento existe para no dejar ambiguo: Copilot no tiene scope global por
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentic-workflow-manager",
3
- "version": "8.0.0",
3
+ "version": "8.1.1",
4
4
  "main": "dist/src/index.js",
5
5
  "bin": {
6
6
  "awm": "./dist/src/index.js"
@@ -27,7 +27,8 @@
27
27
  "release": "node dist/src/release/index.js",
28
28
  "prepack": "npm run build",
29
29
  "prepublishOnly": "npm run build",
30
- "docs:matrix": "ts-node scripts/support-matrix.ts && ts-node scripts/sensor-support-matrix.ts --registry-root tests/fixtures/sensor-support-matrix/registry"
30
+ "docs:matrix": "ts-node scripts/support-matrix.ts && ts-node scripts/sensor-support-matrix.ts --registry-root ../../awm-baseline-registry --registry-tag v2.0.0 --registry-commit c35c087a0801c0b4e69e0a4ac3eafef9ecdf37cd",
31
+ "docs:matrix:prepublication": "ts-node scripts/sensor-support-matrix.ts --registry-root tests/fixtures/sensor-support-matrix/registry --prepublication-fixture --stdout"
31
32
  },
32
33
  "keywords": [
33
34
  "agentic",