agentic-workflow-manager 8.1.4 → 8.1.6

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 (35) 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 +140 -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/tests/commands/preflight/preflight.test.js +50 -0
  16. package/dist/tests/commands/sensors/baseline.test.js +14 -0
  17. package/dist/tests/commands/sensors/changed-windows.test.js +3 -0
  18. package/dist/tests/commands/sensors/compatibility/contract.test.js +51 -0
  19. package/dist/tests/commands/sensors/compatibility/manifest.test.js +27 -11
  20. package/dist/tests/commands/sensors/compatibility/probe.test.js +13 -3
  21. package/dist/tests/commands/sensors/exec-fixtures.js +1 -1
  22. package/dist/tests/commands/sensors/exec.test.js +36 -0
  23. package/dist/tests/commands/sensors/index.test.js +46 -8
  24. package/dist/tests/commands/sensors/init.test.js +30 -1
  25. package/dist/tests/commands/sensors/prepare.test.js +88 -0
  26. package/dist/tests/commands/sensors/router.test.js +15 -1
  27. package/dist/tests/commands/sensors/run-changed.test.js +4 -4
  28. package/dist/tests/commands/sensors/run.test.js +29 -0
  29. package/dist/tests/commands/sensors/status-windows.test.js +1 -1
  30. package/dist/tests/commands/sensors/status.test.js +52 -20
  31. package/dist/tests/integration/preflight-json-pipe.e2e.test.js +79 -3
  32. package/dist/tests/integration/sensor-compatibility.e2e.test.js +40 -1
  33. package/dist/tests/structural/sensor-documentation-contract.test.js +33 -5
  34. package/dist/tests/structural/support-matrix-is-current.test.js +25 -0
  35. package/package.json +1 -1
@@ -0,0 +1,88 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const prepare_1 = require("../../../src/commands/sensors/prepare");
4
+ const fullCommand = { executable: 'manifest-eslint', resolution: 'path', args: ['.'] };
5
+ const changedCommand = {
6
+ executable: 'live-eslint',
7
+ resolution: 'path',
8
+ args: ['--format', 'json', '{files}'],
9
+ fileInput: { placeholder: '{files}', extensions: ['.ts'] },
10
+ };
11
+ function v2Input(overrides = {}) {
12
+ const liveVariant = {
13
+ id: 'eslint-live', priority: 1, certifiedRange: '>=1.0.0', requirements: { tool: 'eslint', toolRange: '>=1.0.0', runtime: 'node', runtimeRange: '>=1.0.0' }, assets: [],
14
+ probe: { kind: 'version' }, command: { executable: 'live-eslint', resolution: 'path', args: ['.'] }, changedCommand, formatter: 'eslint-llm',
15
+ };
16
+ return {
17
+ name: 'lint',
18
+ sensor: {
19
+ enabled: true, fast: false, variantId: 'eslint-live', command: fullCommand,
20
+ initializedCompatibility: { state: 'certified', reason: 'test', variantId: 'eslint-live', toolVersion: '1.0.0', runtimeVersion: '1.0.0', certifiedRange: '>=1.0.0', evidence: [] },
21
+ },
22
+ liveSensor: { applicability: {}, fast: false, timeout: 30_000, variants: [liveVariant] },
23
+ liveState: { state: 'certified', reason: 'test', variantId: 'eslint-live', toolVersion: '1.0.0', runtimeVersion: '1.0.0', certifiedRange: '>=1.0.0', evidence: [] },
24
+ changed: { files: ['src/a.ts'] },
25
+ requestedScope: 'changed',
26
+ projectTimeout: 90_000,
27
+ ...overrides,
28
+ };
29
+ }
30
+ describe('prepareV2Sensor', () => {
31
+ test('v2 uses the live command and project > pack > fallback timeout (R1.1, R3.1)', () => {
32
+ const prepared = (0, prepare_1.prepareV2Sensor)(v2Input());
33
+ expect(prepared.command).toEqual({ kind: 'structured', value: { executable: 'live-eslint', resolution: 'path', args: ['--format', 'json', 'src/a.ts'] } });
34
+ expect(prepared.timeoutMs).toBe(90_000);
35
+ expect(prepared.timeoutSource).toBe('project');
36
+ expect((0, prepare_1.prepareV2Sensor)(v2Input({ projectTimeout: undefined })).timeoutSource).toBe('pack');
37
+ const fallback = v2Input();
38
+ fallback.liveSensor.timeout = undefined;
39
+ fallback.liveSensor.fast = true;
40
+ fallback.sensor.fast = undefined;
41
+ expect((0, prepare_1.prepareV2Sensor)({ ...fallback, projectTimeout: undefined })).toMatchObject({ timeoutMs: 10_000, timeoutSource: 'fallback' });
42
+ });
43
+ test('expands changed paths as literal argv entries (R4.1, R10.2)', () => {
44
+ const prepared = (0, prepare_1.prepareV2Sensor)(v2Input({ changed: { files: ['src/a b.ts', 'src/$x.ts'] } }));
45
+ expect(prepared.command).toEqual({ kind: 'structured', value: { executable: 'live-eslint', resolution: 'path', args: ['--format', 'json', 'src/a b.ts', 'src/$x.ts'] } });
46
+ expect(prepared.effectiveScope).toBe('changed');
47
+ });
48
+ test('falls back full with an explicit reason without changedCommand (R4.2)', () => {
49
+ const input = v2Input();
50
+ input.liveSensor.variants[0].changedCommand = undefined;
51
+ const prepared = (0, prepare_1.prepareV2Sensor)(input);
52
+ expect(prepared.command).toEqual({ kind: 'structured', value: input.liveSensor.variants[0].command });
53
+ expect(prepared.effectiveScope).toBe('full');
54
+ expect(prepared.scopeReason).toMatch(/does not support changed scope/);
55
+ });
56
+ test('uses the full command with an explicit reason when the diff cannot resolve', () => {
57
+ const prepared = (0, prepare_1.prepareV2Sensor)(v2Input({ changed: { files: [], error: 'git failed' } }));
58
+ expect(prepared.effectiveScope).toBe('full');
59
+ expect(prepared.scopeReason).toMatch(/could not be resolved: git failed/);
60
+ });
61
+ test('returns zero-file pass plan without a process (R4.4)', () => {
62
+ const prepared = (0, prepare_1.prepareV2Sensor)(v2Input({ changed: { files: ['README.md'] } }));
63
+ expect(prepared).toMatchObject({ effectiveScope: 'changed', files: 0, syntheticStatus: 'pass' });
64
+ expect(prepared.command).toBeUndefined();
65
+ });
66
+ test('rejects an invalid requested scope before preparing a command', () => {
67
+ expect(() => (0, prepare_1.prepareV2Sensor)({ ...v2Input(), requestedScope: 'sideways' }))
68
+ .toThrow('requested scope must be "full" or "changed"');
69
+ });
70
+ });
71
+ describe('prepareLegacySensor', () => {
72
+ const originalPlatform = process.platform;
73
+ afterEach(() => Object.defineProperty(process, 'platform', { value: originalPlatform, configurable: true }));
74
+ test('falls back to the full command for an unsafe Windows filename', () => {
75
+ Object.defineProperty(process, 'platform', { value: 'win32', configurable: true });
76
+ const prepared = (0, prepare_1.prepareLegacySensor)({
77
+ name: 'lint', config: { cmd: 'eslint .', changedCmd: 'eslint {files}' }, requestedScope: 'changed', changed: { files: ['src/a&b.ts'] },
78
+ });
79
+ expect(prepared).toMatchObject({ command: { kind: 'legacy', value: 'eslint .' }, effectiveScope: 'full' });
80
+ expect(prepared.scopeReason).toMatch(/cmd\.exe metacharacter/);
81
+ });
82
+ });
83
+ describe('validateRunOptions', () => {
84
+ test('rejects changed baseline capture before any scope preparation (R4.6)', () => {
85
+ expect(() => (0, prepare_1.validateRunOptions)({ changed: true, ignoreBaseline: true }))
86
+ .toThrow(/refusing to combine --changed with a baseline capture/);
87
+ });
88
+ });
@@ -4,7 +4,7 @@ jest.mock('@clack/prompts', () => ({ log: { success: jest.fn(), info: jest.fn()
4
4
  jest.mock('picocolors', () => ({ green: (s) => s, yellow: (s) => s, red: (s) => s }));
5
5
  jest.mock('../../../src/commands/sensors/run', () => ({ runSensors: jest.fn().mockReturnValue({ sensors: [], overall: 'pass' }) }));
6
6
  jest.mock('../../../src/commands/sensors/init', () => ({ initSensors: jest.fn().mockReturnValue({ detection: { pack: 'js-ts', indicators: [] }, manifest: { sensors: {} }, configured: [] }) }));
7
- jest.mock('../../../src/commands/sensors/status', () => ({ computeSensorStatus: jest.fn().mockReturnValue({ overall: 'HEALTHY', pack: 'js-ts', checks: {} }) }));
7
+ jest.mock('../../../src/commands/sensors/status', () => ({ computeSensorStatus: jest.fn().mockReturnValue({ overall: 'READY', pack: 'js-ts', checks: {} }) }));
8
8
  jest.mock('../../../src/commands/sensors/install', () => ({ installSensorHook: jest.fn().mockReturnValue({ status: 'installed' }) }));
9
9
  const commander_1 = require("commander");
10
10
  const index_1 = require("../../../src/commands/sensors/index");
@@ -21,4 +21,18 @@ describe('registerSensorsCommand', () => {
21
21
  expect(subNames).toContain('install');
22
22
  expect(subNames).toContain('coverage');
23
23
  });
24
+ it('renders READY without claiming sensor execution, HEALTHY, or project certification', async () => {
25
+ const program = new commander_1.Command();
26
+ const output = jest.spyOn(console, 'log').mockImplementation(() => undefined);
27
+ try {
28
+ (0, index_1.registerSensorsCommand)(program);
29
+ await program.parseAsync(['node', 'awm', 'sensors', 'status']);
30
+ const rendered = output.mock.calls.flat().join('\n');
31
+ expect(rendered).toContain('READY');
32
+ expect(rendered).not.toMatch(/HEALTHY|certif/i);
33
+ }
34
+ finally {
35
+ output.mockRestore();
36
+ }
37
+ });
24
38
  });
@@ -84,12 +84,12 @@ describe('runSensors --changed', () => {
84
84
  expect(out.sensors[0].scope).toBeUndefined();
85
85
  expect(out.changedScope).toEqual({ files: 0, error: 'not a git repository' });
86
86
  });
87
- it('skips an opted-in sensor when nothing changed, without touching the others', async () => {
87
+ it('records a clean synthetic pass when an opted-in sensor has no changed files, without touching the others', async () => {
88
88
  dir = project({ lint: LINT, typecheck: TYPECHECK });
89
89
  mockChangedFiles.mockReturnValue({ files: [] });
90
90
  const out = await load().runSensors({ cwd: dir, changed: true });
91
91
  const lint = out.sensors.find((s) => s.name === 'lint');
92
- expect(lint.status).toBe('skipped');
92
+ expect(lint.status).toBe('pass');
93
93
  expect(lint.skipReason).toBe('no changed files in scope');
94
94
  expect(cmds()).toEqual(['tsc --noEmit']);
95
95
  });
@@ -101,13 +101,13 @@ describe('runSensors --changed', () => {
101
101
  await load().runSensors({ cwd: dir, changed: true });
102
102
  expect(cmds()).toEqual([`eslint --format json 'src/a.ts'`]);
103
103
  });
104
- it('skips the sensor when the filter empties the scope, rather than running repo-wide', async () => {
104
+ it('records a clean synthetic pass when the filter empties the scope, rather than running repo-wide', async () => {
105
105
  // A docs-only commit means the lint sensor has nothing to say. Falling back to
106
106
  // the full command here would reintroduce exactly the cost --changed removes.
107
107
  dir = project({ lint: { ...LINT, changedExtensions: ['.ts'] }, typecheck: TYPECHECK });
108
108
  mockChangedFiles.mockReturnValue({ files: ['README.md'] });
109
109
  const out = await load().runSensors({ cwd: dir, changed: true });
110
- expect(out.sensors.find((s) => s.name === 'lint').status).toBe('skipped');
110
+ expect(out.sensors.find((s) => s.name === 'lint').status).toBe('pass');
111
111
  expect(cmds()).toEqual(['tsc --noEmit']);
112
112
  });
113
113
  it('refuses a changedCmd without a {files} placeholder instead of running it repo-wide', async () => {
@@ -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 {
@@ -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.4",
3
+ "version": "8.1.6",
4
4
  "main": "dist/src/index.js",
5
5
  "bin": {
6
6
  "awm": "./dist/src/index.js"