agentic-workflow-manager 6.5.2 → 8.1.0

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 (95) hide show
  1. package/dist/scripts/sensor-support-matrix.js +103 -0
  2. package/dist/scripts/support-matrix.js +40 -17
  3. package/dist/src/commands/init.js +1 -3
  4. package/dist/src/commands/ledger/index.js +6 -0
  5. package/dist/src/commands/preflight/checks.js +4 -4
  6. package/dist/src/commands/preflight/index.js +2 -2
  7. package/dist/src/commands/registry/add.js +2 -2
  8. package/dist/src/commands/registry/index.js +5 -0
  9. package/dist/src/commands/registry/status.js +7 -0
  10. package/dist/src/commands/sensors/compatibility/contract.js +382 -0
  11. package/dist/src/commands/sensors/compatibility/discovery.js +207 -0
  12. package/dist/src/commands/sensors/compatibility/live.js +92 -0
  13. package/dist/src/commands/sensors/compatibility/manifest.js +207 -0
  14. package/dist/src/commands/sensors/compatibility/materialize.js +134 -0
  15. package/dist/src/commands/sensors/compatibility/pack-source.js +78 -0
  16. package/dist/src/commands/sensors/compatibility/probe.js +47 -0
  17. package/dist/src/commands/sensors/compatibility/resolve.js +99 -0
  18. package/dist/src/commands/sensors/compatibility/types.js +2 -0
  19. package/dist/src/commands/sensors/coverage/contract.js +0 -50
  20. package/dist/src/commands/sensors/coverage/empirical.js +131 -0
  21. package/dist/src/commands/sensors/coverage/evaluate.js +85 -7
  22. package/dist/src/commands/sensors/coverage/evidence.js +5 -1
  23. package/dist/src/commands/sensors/coverage/index.js +44 -7
  24. package/dist/src/commands/sensors/coverage/render.js +214 -14
  25. package/dist/src/commands/sensors/coverage/resolve.js +24 -38
  26. package/dist/src/commands/sensors/exec.js +244 -9
  27. package/dist/src/commands/sensors/index.js +18 -6
  28. package/dist/src/commands/sensors/init.js +95 -6
  29. package/dist/src/commands/sensors/run.js +71 -0
  30. package/dist/src/commands/sensors/status.js +48 -4
  31. package/dist/src/commands/sync.js +7 -0
  32. package/dist/src/core/bundles.js +38 -3
  33. package/dist/src/core/init/steps.js +7 -4
  34. package/dist/src/core/ledger/scan.js +228 -0
  35. package/dist/src/core/ledger/store.js +89 -25
  36. package/dist/src/core/ledger/types.js +54 -0
  37. package/dist/src/core/paths.js +10 -9
  38. package/dist/src/core/registries.js +134 -14
  39. package/dist/src/index.js +24 -1
  40. package/dist/tests/commands/ledger/index.test.js +11 -0
  41. package/dist/tests/commands/preflight/preflight.test.js +64 -64
  42. package/dist/tests/commands/registry/add.test.js +69 -2
  43. package/dist/tests/commands/registry/status.test.js +14 -0
  44. package/dist/tests/commands/sensors/compatibility/contract.test.js +193 -0
  45. package/dist/tests/commands/sensors/compatibility/discovery.test.js +119 -0
  46. package/dist/tests/commands/sensors/compatibility/live.test.js +124 -0
  47. package/dist/tests/commands/sensors/compatibility/manifest.test.js +77 -0
  48. package/dist/tests/commands/sensors/compatibility/materialize.test.js +72 -0
  49. package/dist/tests/commands/sensors/compatibility/pack-source.test.js +55 -0
  50. package/dist/tests/commands/sensors/compatibility/probe.test.js +37 -0
  51. package/dist/tests/commands/sensors/compatibility/python-venv-fixture.js +29 -0
  52. package/dist/tests/commands/sensors/compatibility/resolve.test.js +83 -0
  53. package/dist/tests/commands/sensors/coverage/contract.test.js +8 -22
  54. package/dist/tests/commands/sensors/coverage/empirical.test.js +111 -0
  55. package/dist/tests/commands/sensors/coverage/evaluate.test.js +29 -0
  56. package/dist/tests/commands/sensors/coverage/evidence.test.js +9 -0
  57. package/dist/tests/commands/sensors/coverage/index.test.js +40 -13
  58. package/dist/tests/commands/sensors/coverage/render.test.js +187 -12
  59. package/dist/tests/commands/sensors/coverage/resolve.test.js +25 -4
  60. package/dist/tests/commands/sensors/exec-windows.test.js +140 -2
  61. package/dist/tests/commands/sensors/exec.test.js +115 -5
  62. package/dist/tests/commands/sensors/index.test.js +8 -0
  63. package/dist/tests/commands/sensors/init-pack-unavailable.test.js +12 -12
  64. package/dist/tests/commands/sensors/init.test.js +204 -48
  65. package/dist/tests/commands/sensors/run-inconclusive.test.js +9 -2
  66. package/dist/tests/commands/sensors/run-is-read-only.test.js +0 -0
  67. package/dist/tests/commands/sensors/run.test.js +66 -3
  68. package/dist/tests/commands/sensors/status-windows.test.js +7 -7
  69. package/dist/tests/commands/sensors/status.test.js +74 -23
  70. package/dist/tests/commands/update.test.js +1 -1
  71. package/dist/tests/core/bundles.test.js +37 -0
  72. package/dist/tests/core/discovery.test.js +4 -0
  73. package/dist/tests/core/init/orchestrator.test.js +5 -1
  74. package/dist/tests/core/init/steps.test.js +20 -4
  75. package/dist/tests/core/ledger/scan.test.js +217 -0
  76. package/dist/tests/core/ledger/store.test.js +108 -0
  77. package/dist/tests/core/profile-pins.test.js +2 -1
  78. package/dist/tests/core/project-skill-links.test.js +22 -0
  79. package/dist/tests/core/registries-capability.test.js +10 -0
  80. package/dist/tests/core/registries-sync.test.js +16 -0
  81. package/dist/tests/core/registries.test.js +129 -1
  82. package/dist/tests/core/registry-manifest.test.js +7 -0
  83. package/dist/tests/core/same-existing-path.test.js +35 -0
  84. package/dist/tests/core/sync-gates.test.js +2 -1
  85. package/dist/tests/integration/copilot-init-isolated.test.js +18 -0
  86. package/dist/tests/integration/sensor-compatibility.e2e.test.js +141 -0
  87. package/dist/tests/integration/sensor-coverage.e2e.test.js +35 -6
  88. package/dist/tests/integration/sensor-structured-run.e2e.test.js +127 -0
  89. package/dist/tests/structural/active-documentation.test.js +128 -0
  90. package/dist/tests/structural/async-entrypoint.test.js +15 -0
  91. package/dist/tests/structural/jest-environment-is-isolated.test.js +44 -0
  92. package/dist/tests/structural/r3-cli-major-version.test.js +37 -0
  93. package/dist/tests/structural/sensor-documentation-contract.test.js +77 -0
  94. package/dist/tests/structural/support-matrix-is-current.test.js +69 -2
  95. package/package.json +8 -2
@@ -52,6 +52,14 @@ describe('sensors coverage Commander wiring', () => {
52
52
  expect(render_1.renderCoverageHuman).toHaveBeenCalledWith(report);
53
53
  expect(stdoutWrite).toHaveBeenCalledWith('human\n');
54
54
  });
55
+ it.each(['0', '-1', '1.5', '2x', 'Infinity', 'NaN', '9007199254740992'])('Commander rejects unsafe --min %s before coverage I/O (R5.5)', async (value) => {
56
+ await expect(programWithSensors().parseAsync(['node', 'awm', 'sensors', 'coverage', '--min', value])).rejects.toThrow('--min must be a positive safe integer');
57
+ expect(coverage_1.runCoverage).not.toHaveBeenCalled();
58
+ });
59
+ it('passes --min as a positive integer to coverage', async () => {
60
+ await programWithSensors().parseAsync(['node', 'awm', 'sensors', 'coverage', '--min', '3']);
61
+ expect(coverage_1.runCoverage).toHaveBeenCalledWith(process.cwd(), {}, { min: 3 });
62
+ });
55
63
  it('emits JSON for --json and does not exit for gaps or inconclusive (R2.9)', async () => {
56
64
  for (const overall of ['gaps', 'inconclusive']) {
57
65
  coverage_1.runCoverage.mockReturnValue({ ...report, overall, static: { ...report.static, status: overall } });
@@ -47,49 +47,49 @@ describe('awm sensors init — detected pack missing from the registry', () => {
47
47
  const manifestOf = (dir) => JSON.parse(fs_1.default.readFileSync(path_1.default.join(dir, '.awm', 'sensors.json'), 'utf-8'));
48
48
  afterAll(() => { for (const d of made)
49
49
  fs_1.default.rmSync(d, { recursive: true, force: true }); });
50
- it('names the missing pack instead of writing an empty manifest silently', () => {
50
+ it('names the missing pack instead of writing an empty manifest silently', async () => {
51
51
  const cwd = project();
52
- const result = (0, init_1.initSensors)({ cwd, registryRoot: registry({ 'js-ts': {}, generic: GENERIC }) });
52
+ const result = await (0, init_1.initSensors)({ cwd, registryRoot: registry({ 'js-ts': {}, generic: GENERIC }) });
53
53
  expect(result.unavailablePack).toBe('python');
54
54
  expect(result.detection.pack).toBe('python'); // what the tree actually is — unchanged
55
55
  });
56
- it('falls back to a pack the registry does have, so the gate still measures something', () => {
56
+ it('falls back to a pack the registry does have, so the gate still measures something', async () => {
57
57
  const cwd = project();
58
- const result = (0, init_1.initSensors)({ cwd, registryRoot: registry({ 'js-ts': {}, generic: GENERIC }) });
58
+ const result = await (0, init_1.initSensors)({ cwd, registryRoot: registry({ 'js-ts': {}, generic: GENERIC }) });
59
59
  expect(result.manifest.pack).toBe('generic');
60
60
  expect(Object.keys(result.manifest.sensors)).toEqual(['security']);
61
61
  expect(manifestOf(cwd).pack).toBe('generic');
62
62
  });
63
- it('stays on the detected pack when the registry has no fallback either', () => {
63
+ it('stays on the detected pack when the registry has no fallback either', async () => {
64
64
  // Nothing better exists. The manifest is honestly empty and still says so —
65
65
  // preflight's `manifest` check fails on `total === 0` with a registry remedy.
66
66
  const cwd = project();
67
- const result = (0, init_1.initSensors)({ cwd, registryRoot: registry({ 'js-ts': {} }) });
67
+ const result = await (0, init_1.initSensors)({ cwd, registryRoot: registry({ 'js-ts': {} }) });
68
68
  expect(result.unavailablePack).toBe('python');
69
69
  expect(result.manifest.pack).toBe('python');
70
70
  expect(result.manifest.sensors).toEqual({});
71
71
  });
72
- it('is quiet when the registry does have the detected pack', () => {
72
+ it('is quiet when the registry does have the detected pack', async () => {
73
73
  const cwd = project();
74
- const result = (0, init_1.initSensors)({ cwd, registryRoot: registry({ python: PYTHON, generic: GENERIC }) });
74
+ const result = await (0, init_1.initSensors)({ cwd, registryRoot: registry({ python: PYTHON, generic: GENERIC }) });
75
75
  expect(result.unavailablePack).toBeUndefined();
76
76
  expect(result.manifest.pack).toBe('python');
77
77
  expect(Object.keys(result.manifest.sensors)).toEqual(['typecheck']);
78
78
  });
79
- it('does not fall back when there is no registry to check against', () => {
79
+ it('does not fall back when there is no registry to check against', async () => {
80
80
  // No registryRoot means nothing to validate against, the same tolerance
81
81
  // `--pack` already has — not evidence that the pack is missing.
82
82
  const cwd = project();
83
- const result = (0, init_1.initSensors)({ cwd });
83
+ const result = await (0, init_1.initSensors)({ cwd });
84
84
  expect(result.unavailablePack).toBeUndefined();
85
85
  expect(result.manifest.pack).toBe('python');
86
86
  });
87
- it('keeps sensors the user already had when falling back', () => {
87
+ it('keeps sensors the user already had when falling back', async () => {
88
88
  // The fallback must not be a way to lose hand-written configuration.
89
89
  const cwd = project();
90
90
  fs_1.default.mkdirSync(path_1.default.join(cwd, '.awm'), { recursive: true });
91
91
  fs_1.default.writeFileSync(path_1.default.join(cwd, '.awm', 'sensors.json'), JSON.stringify({ pack: 'python', sensors: { test: { cmd: 'pytest -q', fast: false } } }));
92
- const result = (0, init_1.initSensors)({ cwd, registryRoot: registry({ generic: GENERIC }) });
92
+ const result = await (0, init_1.initSensors)({ cwd, registryRoot: registry({ generic: GENERIC }) });
93
93
  expect(result.manifest.sensors.test?.cmd).toBe('pytest -q');
94
94
  expect(Object.keys(result.manifest.sensors).sort()).toEqual(['security', 'test']);
95
95
  });
@@ -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() {
@@ -40,69 +41,105 @@ function makePythonRegistry() {
40
41
  }));
41
42
  return registryRoot;
42
43
  }
44
+ function makeV2Registry() {
45
+ const registryRoot = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-reg-v2-'));
46
+ const packDir = path_1.default.join(registryRoot, 'sensor-packs', 'js-ts');
47
+ fs_1.default.mkdirSync(packDir, { recursive: true });
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');
50
+ fs_1.default.writeFileSync(path_1.default.join(packDir, 'pack.json'), JSON.stringify({
51
+ schemaVersion: 2, name: 'js-ts', description: 'fixture', detects: ['package.json'],
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'] } },
54
+ sensors: { lint: { applicability: { allFiles: ['package.json'] }, variants: [{
55
+ id: 'eslint-10', priority: 10, certifiedRange: '>=10.0.0 <11.0.0',
56
+ requirements: { tool: 'eslint', toolRange: '>=10.0.0 <11.0.0', runtime: 'node', runtimeRange: '>=0.0.0' },
57
+ assets: ['eslint.config.awm.mjs'], formatter: 'eslint-llm', probe: { kind: 'config-present' },
58
+ command: { executable: 'eslint', resolution: 'node-modules-bin', args: ['.'] },
59
+ }] } },
60
+ }));
61
+ return registryRoot;
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
+ }
43
80
  describe('detectStack', () => {
44
81
  let tmpDir;
45
82
  beforeEach(() => { tmpDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-init-')); });
46
83
  afterEach(() => { fs_1.default.rmSync(tmpDir, { recursive: true }); });
47
- it('detects js-ts when package.json exists', () => {
84
+ it('detects js-ts when package.json exists', async () => {
48
85
  fs_1.default.writeFileSync(path_1.default.join(tmpDir, 'package.json'), '{}');
49
86
  expect((0, init_1.detectStack)(tmpDir).pack).toBe('js-ts');
50
87
  });
51
- it('detects python when pyproject.toml exists', () => {
88
+ it('detects python when pyproject.toml exists', async () => {
52
89
  fs_1.default.writeFileSync(path_1.default.join(tmpDir, 'pyproject.toml'), '');
53
90
  expect((0, init_1.detectStack)(tmpDir).pack).toBe('python');
54
91
  });
55
- it('falls back to generic when no indicators found', () => {
92
+ it('falls back to generic when no indicators found', async () => {
56
93
  expect((0, init_1.detectStack)(tmpDir).pack).toBe('generic');
57
94
  });
58
- it('detects shell from a root-level *.sh file when no js-ts/python marker exists', () => {
95
+ it('detects shell from a root-level *.sh file when no js-ts/python marker exists', async () => {
59
96
  fs_1.default.writeFileSync(path_1.default.join(tmpDir, 'deploy.sh'), '#!/bin/sh\n');
60
97
  const result = (0, init_1.detectStack)(tmpDir);
61
98
  expect(result.pack).toBe('shell');
62
99
  expect(result.indicators).toEqual(['deploy.sh']);
63
100
  });
64
- it('detects shell from a scripts/*.sh file when root has nothing', () => {
101
+ it('detects shell from a scripts/*.sh file when root has nothing', async () => {
65
102
  fs_1.default.mkdirSync(path_1.default.join(tmpDir, 'scripts'));
66
103
  fs_1.default.writeFileSync(path_1.default.join(tmpDir, 'scripts', 'build.sh'), '#!/bin/sh\n');
67
104
  const result = (0, init_1.detectStack)(tmpDir);
68
105
  expect(result.pack).toBe('shell');
69
106
  expect(result.indicators).toEqual([path_1.default.join('scripts', 'build.sh')]);
70
107
  });
71
- it('js-ts wins over shell when both package.json and a root .sh file exist', () => {
108
+ it('js-ts wins over shell when both package.json and a root .sh file exist', async () => {
72
109
  fs_1.default.writeFileSync(path_1.default.join(tmpDir, 'package.json'), '{}');
73
110
  fs_1.default.writeFileSync(path_1.default.join(tmpDir, 'deploy.sh'), '#!/bin/sh\n');
74
111
  expect((0, init_1.detectStack)(tmpDir).pack).toBe('js-ts');
75
112
  });
76
- it('python wins over shell when both a python marker and a root .sh file exist', () => {
113
+ it('python wins over shell when both a python marker and a root .sh file exist', async () => {
77
114
  fs_1.default.writeFileSync(path_1.default.join(tmpDir, 'pyproject.toml'), '');
78
115
  fs_1.default.writeFileSync(path_1.default.join(tmpDir, 'deploy.sh'), '#!/bin/sh\n');
79
116
  expect((0, init_1.detectStack)(tmpDir).pack).toBe('python');
80
117
  });
81
- it('falls through to generic when scripts/ has only non-.sh files (glob must not over-match)', () => {
118
+ it('falls through to generic when scripts/ has only non-.sh files (glob must not over-match)', async () => {
82
119
  fs_1.default.mkdirSync(path_1.default.join(tmpDir, 'scripts'));
83
120
  fs_1.default.writeFileSync(path_1.default.join(tmpDir, 'scripts', 'notes.txt'), 'not shell');
84
121
  expect((0, init_1.detectStack)(tmpDir).pack).toBe('generic');
85
122
  });
86
- it('detects python from requirements.txt alone', () => {
123
+ it('detects python from requirements.txt alone', async () => {
87
124
  fs_1.default.writeFileSync(path_1.default.join(tmpDir, 'requirements.txt'), '');
88
125
  expect((0, init_1.detectStack)(tmpDir).pack).toBe('python');
89
126
  });
90
- it('detects python from Pipfile alone', () => {
127
+ it('detects python from Pipfile alone', async () => {
91
128
  fs_1.default.writeFileSync(path_1.default.join(tmpDir, 'Pipfile'), '');
92
129
  expect((0, init_1.detectStack)(tmpDir).pack).toBe('python');
93
130
  });
94
- it('python (via Pipfile) wins over shell when both a Pipfile and a root .sh file exist', () => {
131
+ it('python (via Pipfile) wins over shell when both a Pipfile and a root .sh file exist', async () => {
95
132
  fs_1.default.writeFileSync(path_1.default.join(tmpDir, 'Pipfile'), '');
96
133
  fs_1.default.writeFileSync(path_1.default.join(tmpDir, 'deploy.sh'), '#!/bin/sh\n');
97
134
  expect((0, init_1.detectStack)(tmpDir).pack).toBe('python');
98
135
  });
99
- it('does not report a directory named "*.sh" as a shell indicator', () => {
136
+ it('does not report a directory named "*.sh" as a shell indicator', async () => {
100
137
  // Directory literally named `something.sh` (not a file) — findShellIndicators'
101
138
  // `entry.isFile()` guard must exclude it. Nothing else present → generic.
102
139
  fs_1.default.mkdirSync(path_1.default.join(tmpDir, 'something.sh'));
103
140
  expect((0, init_1.detectStack)(tmpDir).pack).toBe('generic');
104
141
  });
105
- it('ignores a directory named "*.sh" but still finds a real .sh file alongside it', () => {
142
+ it('ignores a directory named "*.sh" but still finds a real .sh file alongside it', async () => {
106
143
  fs_1.default.mkdirSync(path_1.default.join(tmpDir, 'notreal.sh'));
107
144
  fs_1.default.writeFileSync(path_1.default.join(tmpDir, 'deploy.sh'), '#!/bin/sh\n');
108
145
  const result = (0, init_1.detectStack)(tmpDir);
@@ -114,16 +151,16 @@ describe('detectSourceDirs', () => {
114
151
  let tmpDir;
115
152
  beforeEach(() => { tmpDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-src-')); });
116
153
  afterEach(() => { fs_1.default.rmSync(tmpDir, { recursive: true }); });
117
- it('returns the App-Router-style dirs that exist', () => {
154
+ it('returns the App-Router-style dirs that exist', async () => {
118
155
  for (const d of ['app', 'lib', 'components'])
119
156
  fs_1.default.mkdirSync(path_1.default.join(tmpDir, d));
120
157
  expect((0, init_1.detectSourceDirs)(tmpDir)).toEqual(['app', 'lib', 'components']);
121
158
  });
122
- it('returns src when it exists', () => {
159
+ it('returns src when it exists', async () => {
123
160
  fs_1.default.mkdirSync(path_1.default.join(tmpDir, 'src'));
124
161
  expect((0, init_1.detectSourceDirs)(tmpDir)).toEqual(['src']);
125
162
  });
126
- it('falls back to ["src"] when no known source dir exists', () => {
163
+ it('falls back to ["src"] when no known source dir exists', async () => {
127
164
  expect((0, init_1.detectSourceDirs)(tmpDir)).toEqual(['src']);
128
165
  });
129
166
  });
@@ -138,26 +175,26 @@ describe('buildManifest', () => {
138
175
  fs_1.default.rmSync(registryRoot, { recursive: true });
139
176
  fs_1.default.rmSync(cwd, { recursive: true });
140
177
  });
141
- it('builds manifest from the pack.json single source', () => {
178
+ it('builds manifest from the pack.json single source', async () => {
142
179
  const m = (0, init_1.buildManifest)('js-ts', undefined, registryRoot, cwd);
143
180
  expect(m.pack).toBe('js-ts');
144
181
  expect(m.sensors.typecheck.cmd).toBe('npx tsc --noEmit');
145
182
  expect(m.sensors.lint.cmd).toContain('--config eslint.config.awm.mjs');
146
183
  });
147
- it('substitutes {{SOURCE_DIRS}} in depcheck with the detected dirs', () => {
184
+ it('substitutes {{SOURCE_DIRS}} in depcheck with the detected dirs', async () => {
148
185
  for (const d of ['app', 'lib'])
149
186
  fs_1.default.mkdirSync(path_1.default.join(cwd, d));
150
187
  const m = (0, init_1.buildManifest)('js-ts', undefined, registryRoot, cwd);
151
188
  expect(m.sensors.depcheck.cmd).toBe('npx depcruise --config .dep-cruiser.awm.js app lib');
152
189
  expect(m.sensors.depcheck.cmd).not.toContain('{{SOURCE_DIRS}}');
153
190
  });
154
- it('merges conservatively — existing sensor commands are preserved', () => {
191
+ it('merges conservatively — existing sensor commands are preserved', async () => {
155
192
  const existing = { pack: 'js-ts', sensors: { typecheck: { cmd: 'custom-tsc', fast: true } } };
156
193
  const m = (0, init_1.buildManifest)('js-ts', existing, registryRoot, cwd);
157
194
  expect(m.sensors.typecheck.cmd).toBe('custom-tsc');
158
195
  expect(m.sensors.lint).toBeDefined();
159
196
  });
160
- it('carries the formatter field through from pack.json into the built manifest', () => {
197
+ it('carries the formatter field through from pack.json into the built manifest', async () => {
161
198
  // readPackDefaults must copy `formatter` the same way it already copies
162
199
  // `changedCmd`/`changedExtensions` — this is what lets run.ts's getFormatter
163
200
  // dispatch by real tool (ruff/mypy/shellcheck) instead of guessing from the
@@ -167,14 +204,14 @@ describe('buildManifest', () => {
167
204
  expect(m.sensors.typecheck.formatter).toBe('tsc');
168
205
  expect(m.sensors.lint.formatter).toBe('eslint-llm');
169
206
  });
170
- it('returns an empty sensors object when the pack has no pack.json in the registry', () => {
207
+ it('returns an empty sensors object when the pack has no pack.json in the registry', async () => {
171
208
  // No FALLBACK_DEFAULTS anymore: `python` has no pack dir in this fixture
172
209
  // registry (only js-ts does — see makeRegistry) → the honest floor is `{}`,
173
210
  // never CLI-hardcoded commands that can drift from what the registry ships.
174
211
  const m = (0, init_1.buildManifest)('python', undefined, registryRoot, cwd);
175
212
  expect(m.sensors).toEqual({});
176
213
  });
177
- it('per-field merge: an existing sensor missing a newer pack field still inherits it', () => {
214
+ it('per-field merge: an existing sensor missing a newer pack field still inherits it', async () => {
178
215
  // Regression for Finding 1: a manifest written by the old FALLBACK_DEFAULTS-era
179
216
  // CLI has `typecheck: { cmd: 'mypy .', fast: true }` — no `formatter`, because
180
217
  // that field didn't exist yet. A naive `{ ...defaults, ...existingSensors }`
@@ -209,37 +246,163 @@ describe('initSensors', () => {
209
246
  fs_1.default.rmSync(tmpDir, { recursive: true });
210
247
  fs_1.default.rmSync(registryRoot, { recursive: true });
211
248
  });
212
- it('creates .awm/sensors.json for js-ts project', () => {
249
+ it('creates .awm/sensors.json for js-ts project', async () => {
213
250
  fs_1.default.writeFileSync(path_1.default.join(tmpDir, 'package.json'), '{}');
214
- const result = (0, init_1.initSensors)({ cwd: tmpDir, registryRoot });
251
+ const result = await (0, init_1.initSensors)({ cwd: tmpDir, registryRoot });
215
252
  expect(fs_1.default.existsSync(path_1.default.join(tmpDir, '.awm', 'sensors.json'))).toBe(true);
216
253
  expect(result.detection.pack).toBe('js-ts');
217
254
  expect(result.manifest.sensors.typecheck).toBeDefined();
218
255
  });
219
- it('is idempotent — existing sensor commands survive re-init', () => {
256
+ it('is idempotent — existing sensor commands survive re-init', async () => {
220
257
  fs_1.default.writeFileSync(path_1.default.join(tmpDir, 'package.json'), '{}');
221
258
  fs_1.default.mkdirSync(path_1.default.join(tmpDir, '.awm'), { recursive: true });
222
259
  const existing = { pack: 'js-ts', sensors: { typecheck: { cmd: 'my-tsc', fast: true } } };
223
260
  fs_1.default.writeFileSync(path_1.default.join(tmpDir, '.awm', 'sensors.json'), JSON.stringify(existing));
224
- (0, init_1.initSensors)({ cwd: tmpDir, registryRoot });
261
+ await (0, init_1.initSensors)({ cwd: tmpDir, registryRoot });
225
262
  const written = JSON.parse(fs_1.default.readFileSync(path_1.default.join(tmpDir, '.awm', 'sensors.json'), 'utf-8'));
226
263
  expect(written.sensors.typecheck.cmd).toBe('my-tsc');
227
264
  expect(written.sensors.lint).toBeDefined(); // new sensor added
228
265
  });
229
- it('copies pack config files into the repo by default (configure on)', () => {
266
+ it('copies pack config files into the repo by default (configure on)', async () => {
230
267
  fs_1.default.writeFileSync(path_1.default.join(tmpDir, 'package.json'), '{}');
231
268
  fs_1.default.writeFileSync(path_1.default.join(registryRoot, 'sensor-packs', 'js-ts', 'tsconfig.awm.json'), '{}');
232
- const result = (0, init_1.initSensors)({ cwd: tmpDir, registryRoot }); // no explicit configure → default true
269
+ const result = await (0, init_1.initSensors)({ cwd: tmpDir, registryRoot }); // no explicit configure → default true
233
270
  expect(result.configured).toContain('tsconfig.awm.json');
234
271
  expect(fs_1.default.existsSync(path_1.default.join(tmpDir, 'tsconfig.awm.json'))).toBe(true);
235
272
  });
236
- it('does NOT copy config files when configure is false', () => {
273
+ it('does NOT copy config files when configure is false', async () => {
237
274
  fs_1.default.writeFileSync(path_1.default.join(tmpDir, 'package.json'), '{}');
238
275
  fs_1.default.writeFileSync(path_1.default.join(registryRoot, 'sensor-packs', 'js-ts', 'tsconfig.awm.json'), '{}');
239
- const result = (0, init_1.initSensors)({ cwd: tmpDir, registryRoot, configure: false });
276
+ const result = await (0, init_1.initSensors)({ cwd: tmpDir, registryRoot, configure: false });
240
277
  expect(result.configured).toEqual([]);
241
278
  expect(fs_1.default.existsSync(path_1.default.join(tmpDir, 'tsconfig.awm.json'))).toBe(false);
242
279
  });
280
+ it('awaits v2 compatibility probes and persists their materialized evidence', async () => {
281
+ const v2Registry = makeV2Registry();
282
+ try {
283
+ fs_1.default.writeFileSync(path_1.default.join(tmpDir, 'package.json'), JSON.stringify({ devDependencies: { eslint: '^10.0.0' } }));
284
+ fs_1.default.mkdirSync(path_1.default.join(tmpDir, 'node_modules', 'eslint'), { recursive: true });
285
+ fs_1.default.writeFileSync(path_1.default.join(tmpDir, 'node_modules', 'eslint', 'package.json'), JSON.stringify({ version: '10.0.0' }));
286
+ const result = await (0, init_1.initSensors)({ cwd: tmpDir, registryRoot: v2Registry });
287
+ const written = JSON.parse(fs_1.default.readFileSync(path_1.default.join(tmpDir, '.awm', 'sensors.json'), 'utf8'));
288
+ expect(result.manifest).toMatchObject({ schemaVersion: 2, sensors: { lint: { variantId: 'eslint-10' } } });
289
+ // `package.json` is a declared detection file, so config-present matches.
290
+ // Without init awaiting the probe the resolver stays `unverifiable`; this
291
+ // exact assertion therefore distinguishes the async probe integration.
292
+ expect(written.sensors.lint.initializedCompatibility).toMatchObject({ variantId: 'eslint-10', state: 'certified', reason: 'range-and-probe' });
293
+ expect(written.registryRoot).toBe(v2Registry);
294
+ expect(fs_1.default.existsSync(path_1.default.join(tmpDir, 'eslint.config.awm.mjs'))).toBe(true);
295
+ }
296
+ finally {
297
+ fs_1.default.rmSync(v2Registry, { recursive: true, force: true });
298
+ }
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
+ });
352
+ it('preserves enabled and fast choices when a valid v2 manifest is re-initialized', async () => {
353
+ const v2Registry = makeV2Registry();
354
+ try {
355
+ fs_1.default.writeFileSync(path_1.default.join(tmpDir, 'package.json'), JSON.stringify({ devDependencies: { eslint: '^10.0.0' } }));
356
+ fs_1.default.mkdirSync(path_1.default.join(tmpDir, 'node_modules', 'eslint'), { recursive: true });
357
+ fs_1.default.writeFileSync(path_1.default.join(tmpDir, 'node_modules', 'eslint', 'package.json'), JSON.stringify({ version: '10.0.0' }));
358
+ await (0, init_1.initSensors)({ cwd: tmpDir, registryRoot: v2Registry });
359
+ const manifestPath = path_1.default.join(tmpDir, '.awm', 'sensors.json');
360
+ const selected = JSON.parse(fs_1.default.readFileSync(manifestPath, 'utf8'));
361
+ selected.sensors.lint.enabled = false;
362
+ selected.sensors.lint.fast = true;
363
+ fs_1.default.writeFileSync(manifestPath, JSON.stringify(selected));
364
+ await (0, init_1.initSensors)({ cwd: tmpDir, registryRoot: v2Registry });
365
+ const written = JSON.parse(fs_1.default.readFileSync(manifestPath, 'utf8'));
366
+ expect(written.sensors.lint).toMatchObject({ enabled: false, fast: true, variantId: 'eslint-10' });
367
+ }
368
+ finally {
369
+ fs_1.default.rmSync(v2Registry, { recursive: true, force: true });
370
+ }
371
+ });
372
+ it('rejects a symlinked v2 pack source instead of reading it through init', async () => {
373
+ const v2Registry = makeV2Registry();
374
+ try {
375
+ const packPath = path_1.default.join(v2Registry, 'sensor-packs', 'js-ts', 'pack.json');
376
+ const outside = path_1.default.join(v2Registry, 'outside-pack.json');
377
+ fs_1.default.renameSync(packPath, outside);
378
+ fs_1.default.symlinkSync(outside, packPath);
379
+ fs_1.default.writeFileSync(path_1.default.join(tmpDir, 'package.json'), '{}');
380
+ await expect((0, init_1.initSensors)({ cwd: tmpDir, registryRoot: v2Registry })).rejects.toThrow(/symbolic|regular|contain/i);
381
+ expect(fs_1.default.existsSync(path_1.default.join(tmpDir, '.awm', 'sensors.json'))).toBe(false);
382
+ }
383
+ finally {
384
+ fs_1.default.rmSync(v2Registry, { recursive: true, force: true });
385
+ }
386
+ });
387
+ it('migrates legacy overrides explicitly without silently certifying or re-enabling them', async () => {
388
+ const v2Registry = makeV2Registry();
389
+ try {
390
+ fs_1.default.writeFileSync(path_1.default.join(tmpDir, 'package.json'), JSON.stringify({ devDependencies: { eslint: '^10.0.0' } }));
391
+ fs_1.default.mkdirSync(path_1.default.join(tmpDir, 'node_modules', 'eslint'), { recursive: true });
392
+ fs_1.default.writeFileSync(path_1.default.join(tmpDir, 'node_modules', 'eslint', 'package.json'), JSON.stringify({ version: '10.0.0' }));
393
+ fs_1.default.mkdirSync(path_1.default.join(tmpDir, '.awm'));
394
+ fs_1.default.writeFileSync(path_1.default.join(tmpDir, '.awm', 'sensors.json'), JSON.stringify({
395
+ pack: 'js-ts', sensors: { lint: { cmd: 'my-company-eslint .', enabled: false, fast: true } },
396
+ }));
397
+ await (0, init_1.initSensors)({ cwd: tmpDir, registryRoot: v2Registry });
398
+ const written = JSON.parse(fs_1.default.readFileSync(path_1.default.join(tmpDir, '.awm', 'sensors.json'), 'utf8'));
399
+ expect(written.sensors.lint).toMatchObject({ enabled: false, fast: true });
400
+ expect(written.sensors.lint.initializedCompatibility).toMatchObject({ state: 'compatible-unverified', reason: expect.stringContaining('legacy custom command') });
401
+ }
402
+ finally {
403
+ fs_1.default.rmSync(v2Registry, { recursive: true, force: true });
404
+ }
405
+ });
243
406
  });
244
407
  describe('initSensors — --pack override', () => {
245
408
  let tmpDir;
@@ -252,38 +415,31 @@ describe('initSensors — --pack override', () => {
252
415
  fs_1.default.rmSync(tmpDir, { recursive: true });
253
416
  fs_1.default.rmSync(registryRoot, { recursive: true });
254
417
  });
255
- it('skips detection and uses the override pack when it exists in the registry', () => {
418
+ it('skips detection and uses the override pack when it exists in the registry', async () => {
256
419
  // No package.json/pyproject.toml here — if detection ran, this would be 'generic'.
257
- const result = (0, init_1.initSensors)({ pack: 'js-ts', registryRoot, cwd: tmpDir });
420
+ const result = await (0, init_1.initSensors)({ pack: 'js-ts', registryRoot, cwd: tmpDir });
258
421
  expect(result.detection.pack).toBe('js-ts');
259
422
  // Indicators must reflect an override, not file-based detection.
260
423
  expect(result.detection.indicators).not.toEqual(['package.json']);
261
424
  expect(result.detection.indicators.join(' ')).toMatch(/pack override/i);
262
425
  });
263
- it('throws listing available packs when the override pack is not in the registry', () => {
264
- expect(() => (0, init_1.initSensors)({ pack: 'bogus', registryRoot, cwd: tmpDir })).toThrow(/js-ts/);
265
- try {
266
- (0, init_1.initSensors)({ pack: 'bogus', registryRoot, cwd: tmpDir });
267
- throw new Error('expected initSensors to throw');
268
- }
269
- catch (e) {
270
- expect(e.message).toContain('bogus');
271
- expect(e.message).toContain('js-ts');
272
- }
426
+ it('throws listing available packs when the override pack is not in the registry', async () => {
427
+ await expect((0, init_1.initSensors)({ pack: 'bogus', registryRoot, cwd: tmpDir })).rejects.toThrow(/js-ts/);
428
+ await expect((0, init_1.initSensors)({ pack: 'bogus', registryRoot, cwd: tmpDir })).rejects.toThrow(/bogus/);
273
429
  });
274
- it('does not throw when no registryRoot is given — nothing to validate against', () => {
275
- expect(() => (0, init_1.initSensors)({ pack: 'anything', cwd: tmpDir })).not.toThrow();
276
- const result = (0, init_1.initSensors)({ pack: 'anything', cwd: tmpDir });
430
+ it('does not throw when no registryRoot is given — nothing to validate against', async () => {
431
+ await expect((0, init_1.initSensors)({ pack: 'anything', cwd: tmpDir })).resolves.toBeDefined();
432
+ const result = await (0, init_1.initSensors)({ pack: 'anything', cwd: tmpDir });
277
433
  expect(result.detection.pack).toBe('anything');
278
434
  });
279
- it('throws a distinct message when the registry root has no sensor-packs directory at all', () => {
435
+ it('throws a distinct message when the registry root has no sensor-packs directory at all', async () => {
280
436
  // Different failure shape from "pack not in the list": the registry root
281
437
  // itself is missing sensor-packs/, so there's no list to show — must say
282
438
  // so plainly instead of reporting an empty `available: `.
283
439
  const emptyRegistryRoot = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-empty-reg-'));
284
440
  try {
285
- expect(() => (0, init_1.initSensors)({ pack: 'js-ts', registryRoot: emptyRegistryRoot, cwd: tmpDir }))
286
- .toThrow(/no sensor-packs directory/);
441
+ await expect((0, init_1.initSensors)({ pack: 'js-ts', registryRoot: emptyRegistryRoot, cwd: tmpDir }))
442
+ .rejects.toThrow(/no sensor-packs directory/);
287
443
  }
288
444
  finally {
289
445
  fs_1.default.rmSync(emptyRegistryRoot, { recursive: true });
@@ -157,7 +157,7 @@ describe('runSensors — inconclusive: a sensor that could not certify is never
157
157
  expect(out.sensors.find((s) => s.name === 'security').status).toBe('inconclusive');
158
158
  expect(out.overall).toBe('not_certified');
159
159
  });
160
- it('does not degrade the verdict for a disabled sensor alongside healthy ones', async () => {
160
+ it('does not let healthy legacy commands certify a run when another sensor is disabled', async () => {
161
161
  fs_1.default.writeFileSync(path_1.default.join(root, '.awm', 'sensors.json'), JSON.stringify({
162
162
  pack: 'js-ts',
163
163
  sensors: {
@@ -168,7 +168,14 @@ describe('runSensors — inconclusive: a sensor that could not certify is never
168
168
  mockRunCommand.mockResolvedValueOnce(ok());
169
169
  const { runSensors } = load();
170
170
  const out = await runSensors({ cwd: root });
171
- expect(out.overall).toBe('pass');
171
+ // `enabled: false` is informational; the non-certification comes solely
172
+ // from this pre-R3 (schema-less) manifest. Legacy commands remain
173
+ // operational, but their shell-backed, unversioned contract cannot issue
174
+ // a certified `pass` verdict (R3 design R1.4 / R7.2).
175
+ expect(out.sensors.find((s) => s.name === 'typecheck').status).toBe('pass');
176
+ expect(out.sensors.find((s) => s.name === 'mutation').status).toBe('skipped');
177
+ expect(out.sensors.find((s) => s.name === 'mutation').skipReason).toBe('disabled');
178
+ expect(out.overall).toBe('not_certified');
172
179
  });
173
180
  it('still refuses to certify a tree whose sensors are all disabled', async () => {
174
181
  fs_1.default.writeFileSync(path_1.default.join(root, '.awm', 'sensors.json'), JSON.stringify({