agentic-workflow-manager 8.1.3 → 8.1.5

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 (38) hide show
  1. package/dist/src/commands/preflight/checks.js +70 -1
  2. package/dist/src/commands/preflight/index.js +6 -5
  3. package/dist/src/commands/sensors/changed.js +15 -0
  4. package/dist/src/commands/sensors/compatibility/contract.js +17 -3
  5. package/dist/src/commands/sensors/compatibility/manifest.js +16 -4
  6. package/dist/src/commands/sensors/compatibility/timeout.js +21 -0
  7. package/dist/src/commands/sensors/exec.js +3 -1
  8. package/dist/src/commands/sensors/index.js +5 -13
  9. package/dist/src/commands/sensors/init.js +1 -0
  10. package/dist/src/commands/sensors/prepare.js +136 -0
  11. package/dist/src/commands/sensors/result.js +140 -0
  12. package/dist/src/commands/sensors/run.js +56 -335
  13. package/dist/src/commands/sensors/status.js +93 -21
  14. package/dist/src/commands/sensors/verdict.js +27 -0
  15. package/dist/src/release/index.js +13 -0
  16. package/dist/src/release/orchestrator.js +2 -1
  17. package/dist/tests/commands/preflight/preflight.test.js +50 -0
  18. package/dist/tests/commands/sensors/baseline.test.js +14 -0
  19. package/dist/tests/commands/sensors/changed-windows.test.js +3 -0
  20. package/dist/tests/commands/sensors/compatibility/contract.test.js +51 -0
  21. package/dist/tests/commands/sensors/compatibility/manifest.test.js +27 -11
  22. package/dist/tests/commands/sensors/compatibility/probe.test.js +13 -3
  23. package/dist/tests/commands/sensors/exec-fixtures.js +1 -1
  24. package/dist/tests/commands/sensors/exec.test.js +17 -0
  25. package/dist/tests/commands/sensors/index.test.js +46 -8
  26. package/dist/tests/commands/sensors/init.test.js +30 -1
  27. package/dist/tests/commands/sensors/prepare.test.js +88 -0
  28. package/dist/tests/commands/sensors/router.test.js +15 -1
  29. package/dist/tests/commands/sensors/run-changed.test.js +4 -4
  30. package/dist/tests/commands/sensors/run.test.js +29 -0
  31. package/dist/tests/commands/sensors/status-windows.test.js +1 -1
  32. package/dist/tests/commands/sensors/status.test.js +52 -20
  33. package/dist/tests/integration/preflight-json-pipe.e2e.test.js +79 -3
  34. package/dist/tests/integration/sensor-compatibility.e2e.test.js +40 -1
  35. package/dist/tests/release/orchestrator.test.js +25 -1
  36. package/dist/tests/structural/sensor-documentation-contract.test.js +33 -5
  37. package/dist/tests/structural/support-matrix-is-current.test.js +25 -0
  38. package/package.json +1 -1
@@ -7,6 +7,7 @@ const fs_1 = __importDefault(require("fs"));
7
7
  const os_1 = __importDefault(require("os"));
8
8
  const path_1 = __importDefault(require("path"));
9
9
  const run_1 = require("../../../src/commands/sensors/run");
10
+ const verdict_1 = require("../../../src/commands/sensors/verdict");
10
11
  function mkTmp() {
11
12
  return fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-sensors-'));
12
13
  }
@@ -53,6 +54,17 @@ describe('runSensors', () => {
53
54
  fs_1.default.rmSync(emptyDir, { recursive: true });
54
55
  }
55
56
  });
57
+ it.each([
58
+ ['fast', { fast: 'yes' }, 'run option fast must be a boolean'],
59
+ ['slow', { slow: 1 }, 'run option slow must be a boolean'],
60
+ ['all', { all: null }, 'run option all must be a boolean'],
61
+ ['cwd', { cwd: ' ' }, 'run option cwd must be a nonempty string'],
62
+ ])('rejects invalid public %s options before selecting or dispatching sensors', async (_name, options, message) => {
63
+ const { runSensors } = load();
64
+ await expect(runSensors(options)).rejects.toThrow(message);
65
+ expect(mockRunCommand).not.toHaveBeenCalled();
66
+ expect(mockRunStructuredCommand).not.toHaveBeenCalled();
67
+ });
56
68
  it('runs only fast sensors with --fast flag', async () => {
57
69
  mockRunCommand.mockResolvedValue(ok());
58
70
  const { runSensors } = load();
@@ -61,6 +73,12 @@ describe('runSensors', () => {
61
73
  expect(result.sensors.some((s) => s.name === 'security')).toBe(false);
62
74
  expect(result.overall).toBe('not_certified');
63
75
  });
76
+ it('runs both fast and slow sensors when --fast and --slow are combined', async () => {
77
+ mockRunCommand.mockResolvedValue(ok());
78
+ const { runSensors } = load();
79
+ const result = await runSensors({ fast: true, slow: true, cwd: tmpDir });
80
+ expect(result.sensors.map((sensor) => sensor.name)).toEqual(['typecheck', 'lint', 'security', 'mutation']);
81
+ });
64
82
  it('returns fail when a fast sensor has errors', async () => {
65
83
  mockRunCommand
66
84
  .mockResolvedValueOnce(exited(1, 'src/a.ts(1,1): error TS0001: Bad type.'))
@@ -203,6 +221,17 @@ describe('runSensors', () => {
203
221
  expect(result.overall).toBe('fail');
204
222
  });
205
223
  });
224
+ describe('reduceVerdict', () => {
225
+ it('lets a failing full sensor outrank an empty changed-scope synthetic pass', () => {
226
+ expect((0, verdict_1.reduceVerdict)([
227
+ { name: 'lint', status: 'pass', errors: [], scope: 'changed' },
228
+ { name: 'typecheck', status: 'fail', errors: [{ message: 'broken' }] },
229
+ ])).toBe('fail');
230
+ });
231
+ it('rejects malformed result statuses instead of treating them as skipped', () => {
232
+ expect(() => (0, verdict_1.reduceVerdict)([{ name: 'lint', status: 'bogus', errors: [] }])).toThrow('sensor result status is invalid');
233
+ });
234
+ });
206
235
  describe('runSensors v2 lifecycle contract', () => {
207
236
  let project;
208
237
  let home;
@@ -53,7 +53,7 @@ describe('computeSensorStatus — Windows PATH resolution', () => {
53
53
  // En win32 el usuario escribe `semgrep` y en disco existe `semgrep.cmd`.
54
54
  fs_1.default.writeFileSync(path_1.default.join(pathDir, 'semgrep.cmd'), '@echo off\r\n');
55
55
  const result = await (0, status_1.computeSensorStatus)(tmpDir);
56
- expect(result.overall).toBe('DEGRADED');
56
+ expect(result.overall).toBe('READY');
57
57
  expect(result.checks.security.ok).toBe(true);
58
58
  });
59
59
  it('reporta ok:false en win32 cuando el binario no esta en PATH', async () => {
@@ -7,8 +7,11 @@ 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 status_1 = require("../../../src/commands/sensors/status");
10
- const checks_1 = require("../../../src/commands/preflight/checks");
11
- const run_1 = require("../../../src/commands/sensors/run");
10
+ jest.mock('../../../src/commands/sensors/exec', () => ({
11
+ runCommand: jest.fn(),
12
+ runStructuredCommand: jest.fn(),
13
+ }));
14
+ const { runCommand, runStructuredCommand } = require('../../../src/commands/sensors/exec');
12
15
  // `resolveOnPath` resuelve PATH en proceso (ya no invoca un shell — ver
13
16
  // core/paths.ts). Por eso estos tests controlan un PATH aislado en vez de
14
17
  // mockear `execSync`: ademas de reflejar el mecanismo real, los vuelve
@@ -43,13 +46,35 @@ describe('computeSensorStatus', () => {
43
46
  expect(result.overall).toBe('NOT_CONFIGURED');
44
47
  expect(result.pack).toBeNull();
45
48
  });
49
+ it.each([
50
+ ['valid manifest', 'READY', () => {
51
+ installLocalBin('eslint');
52
+ fs_1.default.writeFileSync(path_1.default.join(tmpDir, 'eslint.config.awm.mjs'), 'export default []');
53
+ fs_1.default.mkdirSync(path_1.default.join(tmpDir, '.awm'), { recursive: true });
54
+ fs_1.default.writeFileSync(path_1.default.join(tmpDir, '.awm', 'sensors.json'), JSON.stringify({
55
+ pack: 'js-ts', sensors: { lint: { cmd: 'npx eslint . --config eslint.config.awm.mjs' } },
56
+ }));
57
+ }],
58
+ ['missing tool', 'DEGRADED', () => {
59
+ fs_1.default.mkdirSync(path_1.default.join(tmpDir, '.awm'), { recursive: true });
60
+ fs_1.default.writeFileSync(path_1.default.join(tmpDir, '.awm', 'sensors.json'), JSON.stringify({
61
+ pack: 'js-ts', sensors: { lint: { cmd: 'npx eslint .' } },
62
+ }));
63
+ }],
64
+ ['absent manifest', 'NOT_CONFIGURED', () => { }],
65
+ ])('reports %s as %s without dispatching sensor commands', async (_case, overall, setup) => {
66
+ setup();
67
+ await expect((0, status_1.computeSensorStatus)(tmpDir)).resolves.toMatchObject({ overall });
68
+ expect(runCommand).not.toHaveBeenCalled();
69
+ expect(runStructuredCommand).not.toHaveBeenCalled();
70
+ });
46
71
  // Helper: simulate a tool installed locally (node_modules/.bin/<tool>)
47
72
  function installLocalBin(tool) {
48
73
  const binDir = path_1.default.join(tmpDir, 'node_modules', '.bin');
49
74
  fs_1.default.mkdirSync(binDir, { recursive: true });
50
75
  fs_1.default.writeFileSync(path_1.default.join(binDir, tool), '');
51
76
  }
52
- it('keeps an operational legacy manifest DEGRADED even when its npx tool is installed locally', async () => {
77
+ it('reports READY for an operational legacy manifest when its declared npx tool is installed locally', async () => {
53
78
  installLocalBin('tsc');
54
79
  fs_1.default.mkdirSync(path_1.default.join(tmpDir, '.awm'), { recursive: true });
55
80
  fs_1.default.writeFileSync(path_1.default.join(tmpDir, '.awm', 'sensors.json'), JSON.stringify({
@@ -57,7 +82,7 @@ describe('computeSensorStatus', () => {
57
82
  sensors: { typecheck: { cmd: 'npx tsc --noEmit', fast: true } }
58
83
  }));
59
84
  const result = await (0, status_1.computeSensorStatus)(tmpDir);
60
- expect(result.overall).toBe('DEGRADED');
85
+ expect(result.overall).toBe('READY');
61
86
  expect(result.pack).toBe('js-ts');
62
87
  expect(result.checks.typecheck.ok).toBe(true);
63
88
  });
@@ -84,7 +109,7 @@ describe('computeSensorStatus', () => {
84
109
  expect(result.checks.lint.ok).toBe(false);
85
110
  expect(result.checks.lint.detail).toMatch(/missing config/i);
86
111
  });
87
- it('is HEALTHY when the npx tool is installed and the --config file exists', async () => {
112
+ it('is READY when the declared npx tool and config are present without running a sensor command', async () => {
88
113
  installLocalBin('eslint');
89
114
  fs_1.default.writeFileSync(path_1.default.join(tmpDir, 'eslint.config.awm.mjs'), 'export default []');
90
115
  fs_1.default.mkdirSync(path_1.default.join(tmpDir, '.awm'), { recursive: true });
@@ -93,7 +118,10 @@ describe('computeSensorStatus', () => {
93
118
  sensors: { lint: { cmd: 'npx eslint . --config eslint.config.awm.mjs --format json', fast: true } }
94
119
  }));
95
120
  const result = await (0, status_1.computeSensorStatus)(tmpDir);
121
+ expect(result.overall).toBe('READY');
96
122
  expect(result.checks.lint.ok).toBe(true);
123
+ expect(runCommand).not.toHaveBeenCalled();
124
+ expect(runStructuredCommand).not.toHaveBeenCalled();
97
125
  });
98
126
  it('returns DEGRADED when a binary is missing', async () => {
99
127
  fs_1.default.mkdirSync(path_1.default.join(tmpDir, '.awm'), { recursive: true });
@@ -122,7 +150,7 @@ describe('computeSensorStatus', () => {
122
150
  }));
123
151
  installOnPath('semgrep');
124
152
  const result = await (0, status_1.computeSensorStatus)(tmpDir);
125
- expect(result.overall).toBe('DEGRADED');
153
+ expect(result.overall).toBe('READY');
126
154
  expect(result.checks.security.ok).toBe(true);
127
155
  });
128
156
  });
@@ -164,10 +192,10 @@ describe('computeSensorStatus', () => {
164
192
  expect(result.checks.mutation.ok).toBe(true);
165
193
  expect(result.checks.mutation.detail).toBe('disabled');
166
194
  });
167
- it('does not trust the initialized v2 variant after local tool drift', async () => {
168
- // The manifest records ESLint 9 at init time, but local evidence is now ESLint
169
- // 10. Status must resolve the installed pack again, not certify the stale
170
- // initializedCompatibility record.
195
+ it('degrades a v2 manifest whose live static compatibility has drifted', async () => {
196
+ // The manifest's initialization evidence remains durable diagnostic context.
197
+ // Status only checks its declared local executable; it never re-runs a project
198
+ // compatibility probe or calls a sensor command.
171
199
  const previousHome = process.env.AWM_HOME;
172
200
  const home = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-status-home-'));
173
201
  try {
@@ -178,7 +206,7 @@ describe('computeSensorStatus', () => {
178
206
  const variant = (id, range) => ({
179
207
  id, priority: 10, certifiedRange: range,
180
208
  requirements: { tool: 'eslint', toolRange: range, runtime: 'node', runtimeRange: '>=0.0.0' },
181
- assets: ['eslint.config.awm.mjs'], formatter: 'eslint-llm', probe: { kind: 'config-present' },
209
+ assets: ['eslint.config.awm.mjs'], formatter: 'eslint-llm', probe: { kind: 'package-script-present' },
182
210
  command: { executable: 'eslint', resolution: 'node-modules-bin', args: ['.'] },
183
211
  });
184
212
  fs_1.default.writeFileSync(path_1.default.join(registry, 'sensor-packs', 'js-ts', 'pack.json'), JSON.stringify({
@@ -188,6 +216,8 @@ describe('computeSensorStatus', () => {
188
216
  fs_1.default.writeFileSync(path_1.default.join(tmpDir, 'package.json'), JSON.stringify({ devDependencies: { eslint: '^10.0.0' } }));
189
217
  fs_1.default.mkdirSync(path_1.default.join(tmpDir, 'node_modules', 'eslint'), { recursive: true });
190
218
  fs_1.default.writeFileSync(path_1.default.join(tmpDir, 'node_modules', 'eslint', 'package.json'), JSON.stringify({ version: '10.0.0' }));
219
+ fs_1.default.mkdirSync(path_1.default.join(tmpDir, 'node_modules', '.bin'), { recursive: true });
220
+ fs_1.default.writeFileSync(path_1.default.join(tmpDir, 'node_modules', '.bin', 'eslint'), '');
191
221
  fs_1.default.mkdirSync(path_1.default.join(tmpDir, '.awm'), { recursive: true });
192
222
  fs_1.default.writeFileSync(path_1.default.join(tmpDir, '.awm', 'sensors.json'), JSON.stringify({
193
223
  schemaVersion: 2, pack: 'js-ts', sensors: { lint: {
@@ -196,15 +226,17 @@ describe('computeSensorStatus', () => {
196
226
  } },
197
227
  }));
198
228
  const result = await (0, status_1.computeSensorStatus)(tmpDir);
199
- expect(result.overall).toBe('DEGRADED');
200
- expect(result.checks.lint).toMatchObject({ ok: false, detail: expect.stringContaining('variant-drift') });
201
- fs_1.default.writeFileSync(path_1.default.join(tmpDir, 'AGENTS.md'), '# test\n');
202
- const gate = await (0, checks_1.preflight)(tmpDir);
203
- expect(gate.status).toBe('degraded');
204
- expect(gate.checks.find(check => check.id === 'tools')).toMatchObject({ ok: false, detail: expect.stringContaining('variant-drift') });
205
- const run = await (0, run_1.runSensors)({ cwd: tmpDir, all: true });
206
- expect(run.overall).toBe('not_certified');
207
- expect(run.sensors).toEqual([expect.objectContaining({ name: 'lint', status: 'inconclusive', skipReason: expect.stringContaining('variant-drift') })]);
229
+ expect(result).toMatchObject({ overall: 'DEGRADED', checks: { lint: { ok: false } } });
230
+ expect(result.checks.lint.detail).toMatch(/drift|eslint-10/i);
231
+ // Once the installed tool matches the initialized variant, a failed
232
+ // static probe is still evidence of degraded readiness — it is not
233
+ // a runtime probe and cannot be waived as merely inconclusive.
234
+ fs_1.default.writeFileSync(path_1.default.join(tmpDir, 'node_modules', 'eslint', 'package.json'), JSON.stringify({ version: '9.0.0' }));
235
+ const staticProbeFailure = await (0, status_1.computeSensorStatus)(tmpDir);
236
+ expect(staticProbeFailure).toMatchObject({ overall: 'DEGRADED', checks: { lint: { ok: false } } });
237
+ expect(staticProbeFailure.checks.lint.detail).toMatch(/probe-not-matched|unverifiable/i);
238
+ expect(runCommand).not.toHaveBeenCalled();
239
+ expect(runStructuredCommand).not.toHaveBeenCalled();
208
240
  }
209
241
  finally {
210
242
  if (previousHome === undefined)
@@ -7,20 +7,96 @@ 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 crypto_1 = __importDefault(require("crypto"));
10
11
  const cliRoot = path_1.default.resolve(__dirname, '../..');
11
12
  const bin = path_1.default.join(cliRoot, 'dist', 'src', 'index.js');
12
- test('preserves degraded preflight JSON when stdout is piped', () => {
13
+ test('preserves actionable no-manifest preflight JSON when verify-sensors is piped', () => {
13
14
  const project = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-preflight-pipe-'));
14
15
  try {
15
- const result = (0, child_process_1.spawnSync)(process.execPath, [bin, 'preflight', '--json'], {
16
+ const result = (0, child_process_1.spawnSync)(process.execPath, [bin, 'preflight', '--verify-sensors', '--json'], {
16
17
  cwd: project,
17
18
  encoding: 'utf8',
18
19
  env: { ...process.env, AWM_HOME: path_1.default.join(project, 'awm-home'), AWM_NO_UPDATE_CHECK: '1' },
19
20
  });
20
21
  expect(result.status).toBe(1);
21
- expect(JSON.parse(result.stdout)).toMatchObject({ status: 'not_configured' });
22
+ expect(JSON.parse(result.stdout)).toMatchObject({
23
+ status: 'not_configured',
24
+ checks: expect.arrayContaining([expect.objectContaining({
25
+ id: 'sensors-execution', ok: false,
26
+ detail: 'sensor verdict was not_certified; no sensor established an empirical pass',
27
+ remedy: expect.stringContaining('awm sensors init'),
28
+ })]),
29
+ });
22
30
  }
23
31
  finally {
24
32
  fs_1.default.rmSync(project, { recursive: true, force: true });
25
33
  }
26
34
  });
35
+ function writeJson(file, value) {
36
+ fs_1.default.mkdirSync(path_1.default.dirname(file), { recursive: true });
37
+ fs_1.default.writeFileSync(file, JSON.stringify(value, null, 2));
38
+ }
39
+ function hashTree(root) {
40
+ const hash = crypto_1.default.createHash('sha256');
41
+ const walk = (dir) => {
42
+ for (const entry of fs_1.default.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
43
+ const file = path_1.default.join(dir, entry.name);
44
+ hash.update(path_1.default.relative(root, file));
45
+ if (entry.isDirectory())
46
+ walk(file);
47
+ else if (entry.isFile())
48
+ hash.update(fs_1.default.readFileSync(file));
49
+ }
50
+ };
51
+ walk(root);
52
+ return hash.digest('hex');
53
+ }
54
+ test('verify-sensors degrades parseably for a v2 sensor that exits 2 without findings and does not write the project', () => {
55
+ const root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-preflight-verify-'));
56
+ const project = path_1.default.join(root, 'project');
57
+ const awmHome = path_1.default.join(root, 'awm-home');
58
+ const registry = path_1.default.join(awmHome, 'registries', 'baseline');
59
+ try {
60
+ fs_1.default.mkdirSync(project, { recursive: true });
61
+ fs_1.default.writeFileSync(path_1.default.join(project, 'AGENTS.md'), '# context\n', { encoding: 'utf8', flag: 'w' });
62
+ writeJson(path_1.default.join(project, 'package.json'), { name: 'preflight-verify-fixture', private: true });
63
+ writeJson(path_1.default.join(project, 'node_modules', 'fixture-sensor', 'package.json'), { name: 'fixture-sensor', version: '1.0.0' });
64
+ fs_1.default.writeFileSync(path_1.default.join(project, 'fixture-sensor.config.mjs'), 'export default {};\n');
65
+ fs_1.default.writeFileSync(path_1.default.join(project, 'fixture-sensor.mjs'), 'process.exit(2);\n');
66
+ writeJson(path_1.default.join(registry, 'sensor-packs', 'fixture', 'pack.json'), {
67
+ schemaVersion: 2, name: 'fixture', description: 'preflight exit-2 fixture', detects: ['package.json'],
68
+ sensors: { lint: { applicability: { allFiles: ['package.json'] }, variants: [{
69
+ id: 'fixture-v1', priority: 100,
70
+ requirements: { tool: 'fixture-sensor', toolRange: '>=1 <2', runtime: 'node', runtimeRange: '>=20', configFiles: ['fixture-sensor.config.mjs'] },
71
+ certifiedRange: '>=1 <2', command: { executable: 'node', resolution: 'path', args: ['fixture-sensor.mjs'] },
72
+ assets: ['fixture-sensor.config.mjs'], formatter: 'generic', probe: { kind: 'config-present' },
73
+ }] } },
74
+ coverage: { schemaVersion: 1, classes: {
75
+ 'fixture-output': {
76
+ description: 'fixture output', detectors: [{ sensor: 'lint' }],
77
+ remedy: { summary: 'run fixture', command: 'awm sensors init --pack fixture' },
78
+ },
79
+ } },
80
+ });
81
+ writeJson(path_1.default.join(awmHome, 'registries.json'), [{ name: 'baseline', remote: 'fixture' }]);
82
+ writeJson(path_1.default.join(project, '.awm', 'sensors.json'), {
83
+ schemaVersion: 2, pack: 'fixture', packSelection: 'explicit', sensors: { lint: {
84
+ enabled: true, fast: true, variantId: 'fixture-v1', command: { executable: 'node', resolution: 'path', args: ['fixture-sensor.mjs'] }, assets: ['fixture-sensor.config.mjs'],
85
+ initializedCompatibility: { state: 'certified', reason: 'fixture', variantId: 'fixture-v1', toolVersion: '1.0.0', runtimeVersion: process.versions.node, certifiedRange: '>=1 <2', evidence: [] },
86
+ } },
87
+ });
88
+ const before = hashTree(project);
89
+ const result = (0, child_process_1.spawnSync)(process.execPath, [bin, 'preflight', '--verify-sensors', '--json', '--cwd', project], {
90
+ cwd: project, encoding: 'utf8', env: { ...process.env, AWM_HOME: awmHome, AWM_NO_UPDATE_CHECK: '1' },
91
+ });
92
+ expect(result.status).toBe(1);
93
+ expect(JSON.parse(result.stdout)).toMatchObject({
94
+ status: 'degraded',
95
+ checks: expect.arrayContaining([expect.objectContaining({ id: 'sensors-execution', ok: false, detail: expect.stringMatching(/lint.*exit 2/i) })]),
96
+ });
97
+ expect(hashTree(project)).toBe(before);
98
+ }
99
+ finally {
100
+ fs_1.default.rmSync(root, { recursive: true, force: true });
101
+ }
102
+ });
@@ -58,6 +58,13 @@ function runCli(fixture, ...args) {
58
58
  env: { ...process.env, AWM_HOME: fixture.awmHome, AWM_NO_UPDATE_CHECK: '1' },
59
59
  });
60
60
  }
61
+ function runAwm(fixture, ...args) {
62
+ return (0, child_process_1.spawnSync)(process.execPath, [bin, ...args], {
63
+ cwd: fixture.project,
64
+ encoding: 'utf8',
65
+ env: { ...process.env, AWM_HOME: fixture.awmHome, AWM_NO_UPDATE_CHECK: '1' },
66
+ });
67
+ }
61
68
  function json(result) {
62
69
  expect(result.status).toBe(0);
63
70
  if (!(result.stdout ?? '').trim())
@@ -81,7 +88,7 @@ test.each(['linux', 'darwin', 'win32'])('keeps injected resolver semantics consi
81
88
  throw new Error('fixture must be a v2 pack');
82
89
  const discovered = (0, discovery_1.discoverProjectEvidence)(fixture.project, parsed.pack, { platform: () => platform });
83
90
  const probe = await (0, probe_1.runCompatibilityProbe)({ kind: 'version' }, { cwd: fixture.project, toolExecutable: 'eslint' }, async () => ({
84
- code: 0, signal: null, timedOut: false, overflowed: false, stdout: 'eslint v10.4.1', stderr: '',
91
+ code: 0, signal: null, timedOut: false, overflowed: false, elapsedMs: 0, stdout: 'eslint v10.4.1', stderr: '',
85
92
  }));
86
93
  const result = (0, resolve_1.resolveSensorCompatibility)(parsed.pack.sensors.lint, { ...discovered, probe }, { pack: 'js-ts', sensor: 'lint' });
87
94
  expect(result).toMatchObject({ state: 'certified', variantId: 'eslint-10', toolVersion: '10.4.1' });
@@ -100,6 +107,38 @@ testWithNoFollow('compiled binary dispatches coverage and emits parseable JSON o
100
107
  fs_1.default.rmSync(fixture.root, { recursive: true, force: true });
101
108
  }
102
109
  });
110
+ testWithNoFollow('compiled sensors run returns nonzero while preserving parseable not_certified JSON', () => {
111
+ const fixture = createFixture();
112
+ try {
113
+ fs_1.default.rmSync(path_1.default.join(fixture.project, '.awm', 'sensors.json'));
114
+ const result = runCli(fixture, 'run', '--fast');
115
+ expect(result.status).toBe(1);
116
+ expect(JSON.parse(result.stdout ?? '')).toMatchObject({ sensors: [], overall: 'not_certified' });
117
+ }
118
+ finally {
119
+ fs_1.default.rmSync(fixture.root, { recursive: true, force: true });
120
+ }
121
+ });
122
+ testWithNoFollow('compiled status reports static READY without writing or executing the project sensor (R6, R6.2)', () => {
123
+ const fixture = createFixture();
124
+ try {
125
+ const localBin = path_1.default.join(fixture.project, 'node_modules', '.bin', 'eslint');
126
+ fs_1.default.mkdirSync(path_1.default.dirname(localBin), { recursive: true });
127
+ fs_1.default.writeFileSync(localBin, 'this fixture must never be executed by status\n');
128
+ fs_1.default.copyFileSync(path_1.default.join(fixture.registryRoot, 'sensor-packs', 'js-ts', 'eslint.fixture.mjs'), path_1.default.join(fixture.project, 'eslint.fixture.mjs'));
129
+ const initialized = runCli(fixture, 'init', '--registry-root', fixture.registryRoot, '--pack', 'js-ts', '--no-configure');
130
+ expect(initialized.status).toBe(0);
131
+ const before = hashTree(fixture.project);
132
+ const result = runAwm(fixture, 'sensors', 'status');
133
+ expect(result.status).toBe(0);
134
+ expect(result.stdout).toContain('READY');
135
+ expect(result.stderr).toBe('');
136
+ expect(hashTree(fixture.project)).toBe(before);
137
+ }
138
+ finally {
139
+ fs_1.default.rmSync(fixture.root, { recursive: true, force: true });
140
+ }
141
+ });
103
142
  testWithNoFollow('legacy coverage stays unverified, init migrates explicitly, and version drift is visible (R7.2, R7.8)', () => {
104
143
  const fixture = createFixture();
105
144
  try {
@@ -6,6 +6,10 @@ const core_1 = require("../../src/release/core");
6
6
  function makeIO(over = {}) {
7
7
  const calls = [];
8
8
  let pkgVersion = '2.1.1';
9
+ // El lockfile arranca en la MISMA version que el package.json, como en el repo real.
10
+ // Si el release no lo sincroniza, este valor se queda atras — que es exactamente el
11
+ // estado que dejaba `main` en rojo (#92).
12
+ let lockVersion = '2.1.1';
9
13
  const io = {
10
14
  run(cmd, args) {
11
15
  const full = `${cmd} ${args.join(' ')}`;
@@ -26,6 +30,7 @@ function makeIO(over = {}) {
26
30
  },
27
31
  readPackageVersion: () => pkgVersion,
28
32
  writePackageVersion: (v) => { pkgVersion = v; calls.push(`WRITE_PKG ${v}`); },
33
+ writeLockVersion: (v) => { lockVersion = v; calls.push(`WRITE_LOCK ${v}`); },
29
34
  readChangelog: () => '',
30
35
  writeChangelog: (c) => calls.push(`WRITE_CHANGELOG ${c.split('\n')[0]}`),
31
36
  writeNpmrc: () => calls.push('WRITE_NPMRC'),
@@ -35,7 +40,7 @@ function makeIO(over = {}) {
35
40
  env: { NPM_TOKEN: 'tok' },
36
41
  ...over,
37
42
  };
38
- return { io, calls };
43
+ return { io, calls, versions: () => ({ pkg: pkgVersion, lock: lockVersion }) };
39
44
  }
40
45
  const opts = (o = {}) => ({ dryRun: false, force: null, push: true, branch: 'main', cliDir: '/cli', ...o });
41
46
  describe('release — happy path', () => {
@@ -54,6 +59,25 @@ describe('release — happy path', () => {
54
59
  expect(calls).toContain('WRITE_NPMRC');
55
60
  expect(calls).toContain('REMOVE_NPMRC');
56
61
  });
62
+ // #92: el bump escribia solo package.json, asi que el lockfile quedaba una version
63
+ // atras en CADA release. `r3-cli-major-version.test.ts` exige que coincidan, y el
64
+ // `[skip ci]` del commit de bump hacia que ese rojo no apareciera hasta el PR
65
+ // siguiente — que llegaba roto por algo que no habia hecho.
66
+ it('sincroniza package-lock.json con package.json y lo incluye en el commit de bump', () => {
67
+ const { io, calls, versions } = makeIO({ commits: `feat: x${core_1.US}${core_1.RS}` });
68
+ (0, orchestrator_1.release)(opts(), io);
69
+ expect(versions()).toEqual({ pkg: '2.2.0', lock: '2.2.0' });
70
+ expect(calls).toContain('WRITE_LOCK 2.2.0');
71
+ // Escribir el archivo no alcanza: si no se stagea, el commit de release lo deja
72
+ // fuera y el lockfile queda sucio en el working tree del runner.
73
+ expect(calls).toContain('git add cli/package.json cli/package-lock.json CHANGELOG.md');
74
+ });
75
+ it('no toca el lockfile cuando no hay nada que publicar', () => {
76
+ const { io, calls, versions } = makeIO({ commits: `docs: solo docs${core_1.US}${core_1.RS}` });
77
+ (0, orchestrator_1.release)(opts(), io);
78
+ expect(versions()).toEqual({ pkg: '2.1.1', lock: '2.1.1' });
79
+ expect(calls.some((c) => c.startsWith('WRITE_LOCK'))).toBe(false);
80
+ });
57
81
  it('sin commits releasables → no publica (exit 0 lógico)', () => {
58
82
  const { io, calls } = makeIO({ commits: `docs: solo docs${core_1.US}${core_1.RS}` });
59
83
  const res = (0, orchestrator_1.release)(opts(), io);
@@ -15,7 +15,25 @@ const sensors_1 = require("../../src/commands/sensors");
15
15
  const ROOT = path_1.default.resolve(__dirname, '../../..');
16
16
  const read = (file) => fs_1.default.readFileSync(path_1.default.join(ROOT, file), 'utf8');
17
17
  function documentedJson(files) {
18
- return files.flatMap((file) => Array.from(read(file).matchAll(/```json\s*\n([\s\S]*?)```/g), (match) => JSON.parse(match[1])));
18
+ return files.flatMap((file) => {
19
+ const source = read(file);
20
+ return Array.from(source.matchAll(/```json\s*\n([\s\S]*?)```/g), (match) => {
21
+ const sectionStart = source.lastIndexOf('###', match.index ?? 0);
22
+ const leadIn = source.slice(Math.max(0, sectionStart), match.index ?? 0);
23
+ return {
24
+ value: JSON.parse(match[1]),
25
+ intentionallyIncomplete: /intentionally incomplete fragment/.test(leadIn),
26
+ };
27
+ });
28
+ });
29
+ }
30
+ function documentedV2Manifests(files) {
31
+ return documentedJson(files)
32
+ .filter((example) => !example.intentionallyIncomplete
33
+ && typeof example.value === 'object'
34
+ && example.value !== null
35
+ && example.value.schemaVersion === 2)
36
+ .map(example => example.value);
19
37
  }
20
38
  function helpFor(command) {
21
39
  const program = new commander_1.Command().name('awm');
@@ -38,17 +56,27 @@ describe('R3 canonical sensor documentation', () => {
38
56
  phrases.forEach((phrase) => expect(text).toContain(phrase));
39
57
  });
40
58
  test('parses every documented v2 sensor manifest example with the production parser', () => {
41
- const examples = documentedJson(['docs/configuration.md', 'docs/cli-reference.md'])
42
- .filter((value) => typeof value === 'object' && value !== null && value.schemaVersion === 2);
59
+ const examples = documentedV2Manifests(['docs/configuration.md', 'docs/cli-reference.md']);
43
60
  expect(examples.length).toBeGreaterThan(0);
44
61
  examples.forEach((example) => expect(() => (0, manifest_1.parseSensorManifest)(example, 'documented example')).not.toThrow());
45
62
  });
46
63
  test('keeps documented v2 sensor manifests portable across native platforms', () => {
47
- const examples = documentedJson(['docs/configuration.md', 'docs/cli-reference.md'])
48
- .filter((value) => typeof value === 'object' && value !== null && value.schemaVersion === 2);
64
+ const examples = documentedV2Manifests(['docs/configuration.md', 'docs/cli-reference.md']);
49
65
  expect(examples.length).toBeGreaterThan(0);
50
66
  examples.forEach((example) => expect(example).not.toHaveProperty('registryRoot'));
51
67
  });
68
+ test('excludes only an explicitly incomplete timeout fragment, not invalid full v2 manifests', () => {
69
+ const examples = documentedJson(['docs/configuration.md']);
70
+ const incomplete = examples.find(example => example.intentionallyIncomplete);
71
+ const complete = documentedV2Manifests(['docs/configuration.md']);
72
+ expect(incomplete?.value).toMatchObject({ schemaVersion: 2, sensors: { test: { timeout: 600000 } } });
73
+ expect(() => (0, manifest_1.parseSensorManifest)(incomplete?.value, 'incomplete timeout fragment')).toThrow();
74
+ expect(complete).toHaveLength(1);
75
+ expect(() => (0, manifest_1.parseSensorManifest)(complete[0], 'complete documented manifest')).not.toThrow();
76
+ expect(() => (0, manifest_1.parseSensorManifest)({
77
+ schemaVersion: 2, pack: 'js-ts', sensors: { test: { enabled: true, variantId: 'npm-script' } },
78
+ }, 'invalid full manifest')).toThrow();
79
+ });
52
80
  test('documents exact Commander flags', () => {
53
81
  expect(helpFor('sensors coverage')).toContain('--min <count>');
54
82
  expect(helpFor('ledger add')).toContain('--defect-class <id>');
@@ -24,6 +24,31 @@ const sensor_support_matrix_1 = require("../../scripts/sensor-support-matrix");
24
24
  const SENSOR_FIXTURE_REGISTRY = path_1.default.join(__dirname, '..', 'fixtures', 'sensor-support-matrix', 'registry');
25
25
  const CI_WORKFLOW_PATH = path_1.default.resolve(__dirname, '../../..', '.github', 'workflows', 'ci.yml');
26
26
  describe('docs/support-matrix.md refleja el codigo', () => {
27
+ it('documents the bounded, empirical sensor gate contract (R3-R7, R10)', () => {
28
+ const root = path_1.default.resolve(__dirname, '../../..');
29
+ const cliReference = fs_1.default.readFileSync(path_1.default.join(root, 'docs', 'cli-reference.md'), 'utf8');
30
+ const configuration = fs_1.default.readFileSync(path_1.default.join(root, 'docs', 'configuration.md'), 'utf8');
31
+ const acceptance = fs_1.default.readFileSync(path_1.default.join(root, 'docs', 'testing', 'core-acceptance.md'), 'utf8');
32
+ const osMatrix = fs_1.default.readFileSync(path_1.default.join(root, 'docs', 'testing', 'os-matrix.md'), 'utf8');
33
+ for (const expected of [
34
+ '`execution.timeoutMs`', '`timeoutSource`', '`elapsedMs`',
35
+ '`requestedScope`', '`effectiveScope`', '`files`', '`scopeReason`',
36
+ '`project` → `pack` → `fallback`', '10,000 ms', '120,000 ms',
37
+ '`pass`', '`fail`', '`not_certified`', '`skipped`',
38
+ '`awm preflight --verify-sensors`', 'read-only', 'READY', 'not a health or certification claim',
39
+ ])
40
+ expect(cliReference + configuration + acceptance).toContain(expected);
41
+ expect(acceptance).toContain('legacy manifest');
42
+ expect(acceptance).toContain('v2 manifest without new fields');
43
+ expect(acceptance).toContain('supported, unsupported, empty, and Git-error');
44
+ expect(acceptance).toContain('project, pack, and fallback');
45
+ expect(cliReference).toContain('`not_certified` with an empty sensor list');
46
+ expect(cliReference).toContain('`awm sensors init`');
47
+ expect(acceptance).toContain('established (for example `not_certified` and an empty list)');
48
+ expect(acceptance).toMatch(/must not fabricate a\s+sensor name, timeout, source, or elapsed time/);
49
+ expect(osMatrix).toContain('Ubuntu, macOS, and native Windows');
50
+ expect(osMatrix).toContain('shell-free');
51
+ });
27
52
  it('el bloque generado esta al dia', () => {
28
53
  const doc = fs_1.default.readFileSync(support_matrix_1.DOC_PATH, 'utf-8');
29
54
  const expected = (0, support_matrix_1.spliceGenerated)(doc, (0, support_matrix_1.renderProviderTables)());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentic-workflow-manager",
3
- "version": "8.1.3",
3
+ "version": "8.1.5",
4
4
  "main": "dist/src/index.js",
5
5
  "bin": {
6
6
  "awm": "./dist/src/index.js"